From 1c0e0b0825b2f0e09d0f0e60faa58a6942ce2603 Mon Sep 17 00:00:00 2001 From: ismail Date: Thu, 27 Nov 2025 15:47:03 +0300 Subject: [PATCH] refactore interview --- ATS_PROJECT_LLD.md | 14 +- TESTING_GUIDE.md | 4 +- conftest.py | 6 +- recruitment/admin.py | 52 +- recruitment/forms.py | 1281 +++--- recruitment/migrations/0001_initial.py | 131 +- ...0002_alter_jobposting_job_type_and_more.py | 29 - .../0002_scheduledinterview_interview_type.py | 18 + ...ting_cv_zip_file_jobposting_zip_created.py | 23 - ...ter_interviewschedule_template_location.py | 19 - ...ter_interviewschedule_template_location.py | 19 - .../migrations/0006_alter_customuser_email.py | 18 - recruitment/models.py | 393 +- recruitment/tasks.py | 57 +- recruitment/tests.py | 8 +- recruitment/tests_advanced.py | 14 +- recruitment/urls.py | 282 +- recruitment/views.py | 3984 +++++++++-------- recruitment/views_frontend.py | 95 +- templates/base.html | 2 +- .../interviews/interview_create_onsite.html | 237 + .../interviews/interview_create_remote.html | 74 + .../interview_create_type_selection.html | 54 + templates/interviews/interview_detail.html | 768 ++++ templates/interviews/interview_list.html | 564 ++- .../interviews/partials/interview_list.html | 42 + .../applications_document_review_view.html | 4 +- .../recruitment/applications_exam_view.html | 2 +- .../recruitment/applications_hired_view.html | 8 +- .../applications_interview_view.html | 50 +- .../recruitment/applications_offer_view.html | 2 +- .../applications_screening_view.html | 2 +- 32 files changed, 5081 insertions(+), 3175 deletions(-) delete mode 100644 recruitment/migrations/0002_alter_jobposting_job_type_and_more.py create mode 100644 recruitment/migrations/0002_scheduledinterview_interview_type.py delete mode 100644 recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py delete mode 100644 recruitment/migrations/0004_alter_interviewschedule_template_location.py delete mode 100644 recruitment/migrations/0005_alter_interviewschedule_template_location.py delete mode 100644 recruitment/migrations/0006_alter_customuser_email.py create mode 100644 templates/interviews/interview_create_onsite.html create mode 100644 templates/interviews/interview_create_remote.html create mode 100644 templates/interviews/interview_create_type_selection.html create mode 100644 templates/interviews/interview_detail.html create mode 100644 templates/interviews/partials/interview_list.html diff --git a/ATS_PROJECT_LLD.md b/ATS_PROJECT_LLD.md index f0337f9..9975041 100644 --- a/ATS_PROJECT_LLD.md +++ b/ATS_PROJECT_LLD.md @@ -354,7 +354,7 @@ class ScheduledInterview(Base): candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name="scheduled_interviews") job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="scheduled_interviews") zoom_meeting = models.OneToOneField(ZoomMeeting, on_delete=models.CASCADE, related_name="interview") - schedule = models.ForeignKey(InterviewSchedule, on_delete=models.CASCADE, related_name="interviews", null=True, blank=True) + schedule = models.ForeignKey(BulkInterviewTemplate, on_delete=models.CASCADE, related_name="interviews", null=True, blank=True) interview_date = models.DateField() interview_time = models.TimeField() status = models.CharField(max_length=20, choices=[ @@ -365,9 +365,9 @@ class ScheduledInterview(Base): ], default="scheduled") ``` -#### 2.2.11 InterviewSchedule Model +#### 2.2.11 BulkInterviewTemplate Model ```python -class InterviewSchedule(Base): +class BulkInterviewTemplate(Base): job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="interview_schedules") candidates = models.ManyToManyField(Candidate, related_name="interview_schedules", blank=True, null=True) start_date = models.DateField() @@ -533,7 +533,7 @@ class CandidateService: ### 5.2 Interview Scheduling Logic ```python -class InterviewScheduler: +class BulkInterviewTemplater: @staticmethod def get_available_slots(schedule, date): """Get available interview slots for a specific date""" @@ -915,7 +915,7 @@ class InterviewSchedulingTestCase(TestCase): phone="9876543210", job=self.job ) - self.schedule = InterviewSchedule.objects.create( + self.schedule = BulkInterviewTemplate.objects.create( job=self.job, start_date=timezone.now().date(), end_date=timezone.now().date() + timedelta(days=7), @@ -930,7 +930,7 @@ class InterviewSchedulingTestCase(TestCase): def test_interview_scheduling(self): """Test interview scheduling process""" # Test slot availability - available_slots = InterviewScheduler.get_available_slots( + available_slots = BulkInterviewTemplater.get_available_slots( self.schedule, timezone.now().date() ) @@ -942,7 +942,7 @@ class InterviewSchedulingTestCase(TestCase): 'start_time': timezone.now().time(), 'duration': 60 } - interview = InterviewScheduler.schedule_interview( + interview = BulkInterviewTemplater.schedule_interview( self.candidate, self.job, schedule_data diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index c4b77f3..0616990 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -86,7 +86,7 @@ The test suite aims for 80% code coverage. Coverage reports are generated in: - **Candidate**: Stage transitions, relationships - **ZoomMeeting**: Time validation, status handling - **FormTemplate**: Template integrity, field ordering -- **InterviewSchedule**: Scheduling logic, slot generation +- **BulkInterviewTemplate**: Scheduling logic, slot generation ### 2. View Testing - **Job Management**: CRUD operations, search, filtering @@ -97,7 +97,7 @@ The test suite aims for 80% code coverage. Coverage reports are generated in: ### 3. Form Testing - **JobPostingForm**: Complex validation, field dependencies - **CandidateForm**: File upload, validation -- **InterviewScheduleForm**: Dynamic fields, validation +- **BulkInterviewTemplateForm**: Dynamic fields, validation - **MeetingCommentForm**: Comment creation/editing ### 4. Integration Testing diff --git a/conftest.py b/conftest.py index 864753a..c739946 100644 --- a/conftest.py +++ b/conftest.py @@ -28,13 +28,13 @@ from datetime import datetime, time, timedelta, date from recruitment.models import ( JobPosting, Candidate, ZoomMeeting, FormTemplate, FormStage, FormField, - FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, + FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview, TrainingMaterial, Source, HiringAgency, Profile, MeetingComment, JobPostingImage, BreakTime ) from recruitment.forms import ( JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm, - CandidateStageForm, InterviewScheduleForm, BreakTimeFormSet + CandidateStageForm, BulkInterviewTemplateForm, BreakTimeFormSet ) @@ -185,7 +185,7 @@ def interview_schedule(staff_user, job): ) candidates.append(candidate) - return InterviewSchedule.objects.create( + return BulkInterviewTemplate.objects.create( job=job, created_by=staff_user, start_date=date.today() + timedelta(days=1), diff --git a/recruitment/admin.py b/recruitment/admin.py index 55ccefa..ca09b15 100644 --- a/recruitment/admin.py +++ b/recruitment/admin.py @@ -3,10 +3,10 @@ from django.utils.html import format_html from django.urls import reverse from django.utils import timezone from .models import ( - JobPosting, Application, TrainingMaterial, ZoomMeetingDetails, + JobPosting, Application, TrainingMaterial, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, - SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,JobPostingImage,InterviewNote, - AgencyAccessLink, AgencyJobAssignment + SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,InterviewNote, + AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview ) from django.contrib.auth import get_user_model @@ -158,27 +158,27 @@ class TrainingMaterialAdmin(admin.ModelAdmin): save_on_top = True -@admin.register(ZoomMeetingDetails) -class ZoomMeetingAdmin(admin.ModelAdmin): - list_display = ['topic', 'meeting_id', 'start_time', 'duration', 'created_at'] - list_filter = ['timezone', 'created_at'] - search_fields = ['topic', 'meeting_id'] - readonly_fields = ['created_at', 'updated_at'] - fieldsets = ( - ('Meeting Details', { - 'fields': ('topic', 'meeting_id', 'start_time', 'duration', 'timezone','status') - }), - ('Meeting Settings', { - 'fields': ('participant_video', 'join_before_host', 'mute_upon_entry', 'waiting_room') - }), - ('Access', { - 'fields': ('join_url',) - }), - ('System Response', { - 'fields': ('zoom_gateway_response', 'created_at', 'updated_at') - }), - ) - save_on_top = True +# @admin.register(ZoomMeetingDetails) +# class ZoomMeetingAdmin(admin.ModelAdmin): +# list_display = ['topic', 'meeting_id', 'start_time', 'duration', 'created_at'] +# list_filter = ['timezone', 'created_at'] +# search_fields = ['topic', 'meeting_id'] +# readonly_fields = ['created_at', 'updated_at'] +# fieldsets = ( +# ('Meeting Details', { +# 'fields': ('topic', 'meeting_id', 'start_time', 'duration', 'timezone','status') +# }), +# ('Meeting Settings', { +# 'fields': ('participant_video', 'join_before_host', 'mute_upon_entry', 'waiting_room') +# }), +# ('Access', { +# 'fields': ('join_url',) +# }), +# ('System Response', { +# 'fields': ('zoom_gateway_response', 'created_at', 'updated_at') +# }), +# ) +# save_on_top = True # @admin.register(InterviewNote) @@ -241,9 +241,11 @@ admin.site.register(FormStage) admin.site.register(Application) admin.site.register(FormField) admin.site.register(FieldResponse) -admin.site.register(InterviewSchedule) +admin.site.register(BulkInterviewTemplate) admin.site.register(AgencyAccessLink) admin.site.register(AgencyJobAssignment) +admin.site.register(Interview) +admin.site.register(ScheduledInterview) # AgencyMessage admin removed - model has been deleted diff --git a/recruitment/forms.py b/recruitment/forms.py index 0438cc3..49fa7d8 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -10,12 +10,12 @@ from django.contrib.auth.forms import UserCreationForm User = get_user_model() import re from .models import ( - ZoomMeetingDetails, + #ZoomMeetingDetails, Application, TrainingMaterial, JobPosting, FormTemplate, - InterviewSchedule, + BulkInterviewTemplate, BreakTime, JobPostingImage, InterviewNote, @@ -26,7 +26,7 @@ from .models import ( AgencyAccessLink, Participants, Message, - Person,OnsiteLocationDetails, + Person, Document ) @@ -386,73 +386,73 @@ class ApplicationStageForm(forms.ModelForm): "stage": forms.Select(attrs={"class": "form-select"}), } -class ZoomMeetingForm(forms.ModelForm): - class Meta: - model = ZoomMeetingDetails - fields = ['topic', 'start_time', 'duration'] - labels = { - 'topic': _('Topic'), - 'start_time': _('Start Time'), - 'duration': _('Duration'), - } - widgets = { - 'topic': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter meeting topic'),}), - 'start_time': forms.DateTimeInput(attrs={'class': 'form-control','type': 'datetime-local'}), - 'duration': forms.NumberInput(attrs={'class': 'form-control','min': 1, 'placeholder': _('60')}), - } +# class ZoomMeetingForm(forms.ModelForm): +# class Meta: +# model = ZoomMeetingDetails +# fields = ['topic', 'start_time', 'duration'] +# labels = { +# 'topic': _('Topic'), +# 'start_time': _('Start Time'), +# 'duration': _('Duration'), +# } +# widgets = { +# 'topic': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter meeting topic'),}), +# 'start_time': forms.DateTimeInput(attrs={'class': 'form-control','type': 'datetime-local'}), +# 'duration': forms.NumberInput(attrs={'class': 'form-control','min': 1, 'placeholder': _('60')}), +# } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = 'post' - self.helper.form_class = 'form-horizontal' - self.helper.label_class = 'col-md-3' - self.helper.field_class = 'col-md-9' - self.helper.layout = Layout( - Field('topic', css_class='form-control'), - Field('start_time', css_class='form-control'), - Field('duration', css_class='form-control'), - Submit('submit', _('Create Meeting'), css_class='btn btn-primary') - ) +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.helper = FormHelper() +# self.helper.form_method = 'post' +# self.helper.form_class = 'form-horizontal' +# self.helper.label_class = 'col-md-3' +# self.helper.field_class = 'col-md-9' +# self.helper.layout = Layout( +# Field('topic', css_class='form-control'), +# Field('start_time', css_class='form-control'), +# Field('duration', css_class='form-control'), +# Submit('submit', _('Create Meeting'), css_class='btn btn-primary') +# ) -class MeetingForm(forms.ModelForm): - class Meta: - model = ZoomMeetingDetails - fields = ["topic", "start_time", "duration"] - labels = { - "topic": _("Topic"), - "start_time": _("Start Time"), - "duration": _("Duration"), - } - widgets = { - "topic": forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": _("Enter meeting topic"), - } - ), - "start_time": forms.DateTimeInput( - attrs={"class": "form-control", "type": "datetime-local"} - ), - "duration": forms.NumberInput( - attrs={"class": "form-control", "min": 1, "placeholder": _("60")} - ), - } +# class MeetingForm(forms.ModelForm): +# class Meta: +# model = ZoomMeetingDetails +# fields = ["topic", "start_time", "duration"] +# labels = { +# "topic": _("Topic"), +# "start_time": _("Start Time"), +# "duration": _("Duration"), +# } +# widgets = { +# "topic": forms.TextInput( +# attrs={ +# "class": "form-control", +# "placeholder": _("Enter meeting topic"), +# } +# ), +# "start_time": forms.DateTimeInput( +# attrs={"class": "form-control", "type": "datetime-local"} +# ), +# "duration": forms.NumberInput( +# attrs={"class": "form-control", "min": 1, "placeholder": _("60")} +# ), +# } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = "post" - self.helper.form_class = "form-horizontal" - self.helper.label_class = "col-md-3" - self.helper.field_class = "col-md-9" - self.helper.layout = Layout( - Field("topic", css_class="form-control"), - Field("start_time", css_class="form-control"), - Field("duration", css_class="form-control"), - Submit("submit", _("Create Meeting"), css_class="btn btn-primary"), - ) +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.helper = FormHelper() +# self.helper.form_method = "post" +# self.helper.form_class = "form-horizontal" +# self.helper.label_class = "col-md-3" +# self.helper.field_class = "col-md-9" +# self.helper.layout = Layout( +# Field("topic", css_class="form-control"), +# Field("start_time", css_class="form-control"), +# Field("duration", css_class="form-control"), +# Submit("submit", _("Create Meeting"), css_class="btn btn-primary"), +# ) class TrainingMaterialForm(forms.ModelForm): @@ -699,132 +699,132 @@ class FormTemplateForm(forms.ModelForm): ) -class BreakTimeForm(forms.Form): - """ - A simple Form used for the BreakTimeFormSet. - It is not a ModelForm because the data is stored directly in InterviewSchedule's JSONField, - not in a separate BreakTime model instance. - """ +# class BreakTimeForm(forms.Form): +# """ +# A simple Form used for the BreakTimeFormSet. +# It is not a ModelForm because the data is stored directly in BulkInterviewTemplate's JSONField, +# not in a separate BreakTime model instance. +# """ - start_time = forms.TimeField( - widget=forms.TimeInput(attrs={"type": "time", "class": "form-control"}), - label="Start Time", - ) - end_time = forms.TimeField( - widget=forms.TimeInput(attrs={"type": "time", "class": "form-control"}), - label="End Time", - ) +# start_time = forms.TimeField( +# widget=forms.TimeInput(attrs={"type": "time", "class": "form-control"}), +# label="Start Time", +# ) +# end_time = forms.TimeField( +# widget=forms.TimeInput(attrs={"type": "time", "class": "form-control"}), +# label="End Time", +# ) -BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True) +# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True) -class InterviewScheduleForm(forms.ModelForm): - applications = forms.ModelMultipleChoiceField( - queryset=Application.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=True, - ) - working_days = forms.MultipleChoiceField( - choices=[ - (0, "Monday"), - (1, "Tuesday"), - (2, "Wednesday"), - (3, "Thursday"), - (4, "Friday"), - (5, "Saturday"), - (6, "Sunday"), - ], - widget=forms.CheckboxSelectMultiple, - required=True, - ) +# class BulkInterviewTemplateForm(forms.ModelForm): +# applications = forms.ModelMultipleChoiceField( +# queryset=Application.objects.none(), +# widget=forms.CheckboxSelectMultiple, +# required=True, +# ) +# working_days = forms.MultipleChoiceField( +# choices=[ +# (0, "Monday"), +# (1, "Tuesday"), +# (2, "Wednesday"), +# (3, "Thursday"), +# (4, "Friday"), +# (5, "Saturday"), +# (6, "Sunday"), +# ], +# widget=forms.CheckboxSelectMultiple, +# required=True, +# ) - class Meta: - model = InterviewSchedule - fields = [ - 'schedule_interview_type', - "applications", - "start_date", - "end_date", - "working_days", - "start_time", - "end_time", - "interview_duration", - "buffer_time", - "break_start_time", - "break_end_time", - ] - widgets = { - "start_date": forms.DateInput( - attrs={"type": "date", "class": "form-control"} - ), - "end_date": forms.DateInput( - attrs={"type": "date", "class": "form-control"} - ), - "start_time": forms.TimeInput( - attrs={"type": "time", "class": "form-control"} - ), - "end_time": forms.TimeInput( - attrs={"type": "time", "class": "form-control"} - ), - "interview_duration": forms.NumberInput(attrs={"class": "form-control"}), - "buffer_time": forms.NumberInput(attrs={"class": "form-control"}), - "break_start_time": forms.TimeInput( - attrs={"type": "time", "class": "form-control"} - ), - "break_end_time": forms.TimeInput( - attrs={"type": "time", "class": "form-control"} - ), - "schedule_interview_type":forms.RadioSelect() - } +# class Meta: +# model = BulkInterviewTemplate +# fields = [ +# 'schedule_interview_type', +# "applications", +# "start_date", +# "end_date", +# "working_days", +# "start_time", +# "end_time", +# "interview_duration", +# "buffer_time", +# "break_start_time", +# "break_end_time", +# ] +# widgets = { +# "start_date": forms.DateInput( +# attrs={"type": "date", "class": "form-control"} +# ), +# "end_date": forms.DateInput( +# attrs={"type": "date", "class": "form-control"} +# ), +# "start_time": forms.TimeInput( +# attrs={"type": "time", "class": "form-control"} +# ), +# "end_time": forms.TimeInput( +# attrs={"type": "time", "class": "form-control"} +# ), +# "interview_duration": forms.NumberInput(attrs={"class": "form-control"}), +# "buffer_time": forms.NumberInput(attrs={"class": "form-control"}), +# "break_start_time": forms.TimeInput( +# attrs={"type": "time", "class": "form-control"} +# ), +# "break_end_time": forms.TimeInput( +# attrs={"type": "time", "class": "form-control"} +# ), +# "schedule_interview_type":forms.RadioSelect() +# } - def __init__(self, slug, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["applications"].queryset = Application.objects.filter( - job__slug=slug, stage="Interview" - ) +# def __init__(self, slug, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.fields["applications"].queryset = Application.objects.filter( +# job__slug=slug, stage="Interview" +# ) - def clean_working_days(self): - working_days = self.cleaned_data.get("working_days") - return [int(day) for day in working_days] +# def clean_working_days(self): +# working_days = self.cleaned_data.get("working_days") +# return [int(day) for day in working_days] -class InterviewNoteForm(forms.ModelForm): - """Form for creating and editing meeting comments""" +# class InterviewNoteForm(forms.ModelForm): +# """Form for creating and editing meeting comments""" - class Meta: - model = InterviewNote - fields = ["content"] - widgets = { - "content": CKEditor5Widget( - attrs={ - "class": "form-control", - "placeholder": _("Enter your comment or note"), - }, - config_name="extends", - ), - } - labels = { - "content": _("Comment"), - } +# class Meta: +# model = InterviewNote +# fields = ["content"] +# widgets = { +# "content": CKEditor5Widget( +# attrs={ +# "class": "form-control", +# "placeholder": _("Enter your comment or note"), +# }, +# config_name="extends", +# ), +# } +# labels = { +# "content": _("Comment"), +# } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = "post" - self.helper.form_class = "form-horizontal" - self.helper.label_class = "col-md-3" - self.helper.field_class = "col-md-9" - self.helper.layout = Layout( - Field("content", css_class="form-control"), - Submit("submit", _("Add Comment"), css_class="btn btn-primary mt-3"), - ) +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.helper = FormHelper() +# self.helper.form_method = "post" +# self.helper.form_class = "form-horizontal" +# self.helper.label_class = "col-md-3" +# self.helper.field_class = "col-md-9" +# self.helper.layout = Layout( +# Field("content", css_class="form-control"), +# Submit("submit", _("Add Comment"), css_class="btn btn-primary mt-3"), +# ) -class InterviewForm(forms.ModelForm): - class Meta: - model = ScheduledInterview - fields = ["job", "application"] +# class InterviewForm(forms.ModelForm): +# class Meta: +# model = ScheduledInterview +# fields = ["job", "application"] class ProfileImageUploadForm(forms.ModelForm): @@ -1233,57 +1233,11 @@ class AgencyAccessLinkForm(forms.ModelForm): class AgencyApplicationSubmissionForm(forms.ModelForm): - """Form for agencies to submit candidates (simplified - resume + basic info)""" - - # Person fields for creating/updating person - # first_name = forms.CharField( - # max_length=255, - # widget=forms.TextInput(attrs={ - # "class": "form-control", - # "placeholder": "First Name", - # "required": True, - # }), - # label=_("First Name") - # ) - # last_name = forms.CharField( - # max_length=255, - # widget=forms.TextInput(attrs={ - # "class": "form-control", - # "placeholder": "Last Name", - # "required": True, - # }), - # label=_("Last Name") - # ) - # email = forms.EmailField( - # widget=forms.EmailInput(attrs={ - # "class": "form-control", - # "placeholder": "email@example.com", - # "required": True, - # }), - # label=_("Email Address") - # ) - # phone = forms.CharField( - # max_length=20, - # widget=forms.TextInput(attrs={ - # "class": "form-control", - # "placeholder": "+966 50 123 4567", - # "required": True, - # }), - # label=_("Phone Number") - # ) + """Form for agencies to submit candidates""" class Meta: model = Application - fields = ["person","resume"] - widgets = { - "resume": forms.FileInput( - attrs={ - "class": "form-control", - "accept": ".pdf,.doc,.docx", - "required": True, - } - ), - } + fields = ["person", "resume"] labels = { "resume": _("Resume"), } @@ -1296,47 +1250,14 @@ class AgencyApplicationSubmissionForm(forms.ModelForm): self.helper.form_class = "g-3" self.helper.enctype = "multipart/form-data" - # self.helper.layout = Layout( - # Row( - # Column("first_name", css_class="col-md-6"), - # Column("last_name", css_class="col-md-6"), - # css_class="g-3 mb-3", - # ), - # Row( - # Column("email", css_class="col-md-6"), - # Column("phone", css_class="col-md-6"), - # css_class="g-3 mb-3", - # ), - # Field("resume", css_class="form-control"), - # Div( - # Submit( - # "submit", _("Submit Candidate"), css_class="btn btn-main-action" - # ), - # css_class="col-12 mt-4", - # ), - # ) - - def clean_email(self): - """Validate email format and check for duplicates in the same job""" - email = self.cleaned_data.get("email") - if email: - # Check if person with this email already exists for this job - from .models import Person - existing_person = Person.objects.filter( - email=email.lower().strip() - ).first() - - if existing_person: - # Check if this person already has an application for this job - existing_application = Application.objects.filter( - person=existing_person, job=self.assignment.job - ).first() - - if existing_application: - raise ValidationError( - f"A candidate with this email has already applied for {self.assignment.job.title}." - ) - return email.lower().strip() if email else email + self.helper.layout = Layout( + Field("person", css_class="form-control"), + Field("resume", css_class="form-control"), + Div( + Submit("submit", _("Submit Candidate"), css_class="btn btn-main-action"), + css_class="col-12 mt-4", + ), + ) def clean_resume(self): """Validate resume file""" @@ -1357,26 +1278,7 @@ class AgencyApplicationSubmissionForm(forms.ModelForm): """Override save to set additional fields""" instance = super().save(commit=False) - # Create or get person - from .models import Person - person, created = Person.objects.get_or_create( - email=self.cleaned_data['email'].lower().strip(), - defaults={ - 'first_name': self.cleaned_data['first_name'], - 'last_name': self.cleaned_data['last_name'], - 'phone': self.cleaned_data['phone'], - } - ) - - if not created: - # Update existing person with new info - person.first_name = self.cleaned_data['first_name'] - person.last_name = self.cleaned_data['last_name'] - person.phone = self.cleaned_data['phone'] - person.save() - # Set required fields for agency submission - instance.person = person instance.job = self.assignment.job instance.hiring_agency = self.assignment.agency instance.stage = Application.Stage.APPLIED @@ -1487,57 +1389,57 @@ class PortalLoginForm(forms.Form): # participants form -class ParticipantsForm(forms.ModelForm): - """Form for creating and editing Participants""" +# class ParticipantsForm(forms.ModelForm): +# """Form for creating and editing Participants""" - class Meta: - model = Participants - fields = ["name", "email", "phone", "designation"] - widgets = { - "name": forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "Enter participant name", - "required": True, - } - ), - "email": forms.EmailInput( - attrs={ - "class": "form-control", - "placeholder": "Enter email address", - "required": True, - } - ), - "phone": forms.TextInput( - attrs={"class": "form-control", "placeholder": "Enter phone number"} - ), - "designation": forms.TextInput( - attrs={"class": "form-control", "placeholder": "Enter designation"} - ), - # 'jobs': forms.CheckboxSelectMultiple(), - } +# class Meta: +# model = Participants +# fields = ["name", "email", "phone", "designation"] +# widgets = { +# "name": forms.TextInput( +# attrs={ +# "class": "form-control", +# "placeholder": "Enter participant name", +# "required": True, +# } +# ), +# "email": forms.EmailInput( +# attrs={ +# "class": "form-control", +# "placeholder": "Enter email address", +# "required": True, +# } +# ), +# "phone": forms.TextInput( +# attrs={"class": "form-control", "placeholder": "Enter phone number"} +# ), +# "designation": forms.TextInput( +# attrs={"class": "form-control", "placeholder": "Enter designation"} +# ), +# # 'jobs': forms.CheckboxSelectMultiple(), +# } -class ParticipantsSelectForm(forms.ModelForm): - """Form for selecting Participants""" +# class ParticipantsSelectForm(forms.ModelForm): +# """Form for selecting Participants""" - participants = forms.ModelMultipleChoiceField( - queryset=Participants.objects.all(), - widget=forms.CheckboxSelectMultiple, - required=False, - label=_("Select Participants"), - ) +# participants = forms.ModelMultipleChoiceField( +# queryset=Participants.objects.all(), +# widget=forms.CheckboxSelectMultiple, +# required=False, +# label=_("Select Participants"), +# ) - users = forms.ModelMultipleChoiceField( - queryset=User.objects.all(), - widget=forms.CheckboxSelectMultiple, - required=False, - label=_("Select Users"), - ) +# users = forms.ModelMultipleChoiceField( +# queryset=User.objects.all(), +# widget=forms.CheckboxSelectMultiple, +# required=False, +# label=_("Select Users"), +# ) - class Meta: - model = JobPosting - fields = ["participants", "users"] # No direct fields from Participants model +# class Meta: +# model = JobPosting +# fields = ["participants", "users"] # No direct fields from Participants model @@ -1707,22 +1609,22 @@ class CandidateEmailForm(forms.Form): -class InterviewParticpantsForm(forms.ModelForm): - participants = forms.ModelMultipleChoiceField( - queryset=Participants.objects.all(), - widget=forms.CheckboxSelectMultiple, - required=False , +# class InterviewParticpantsForm(forms.ModelForm): +# participants = forms.ModelMultipleChoiceField( +# queryset=Participants.objects.all(), +# widget=forms.CheckboxSelectMultiple, +# required=False , - ) - system_users=forms.ModelMultipleChoiceField( - queryset=User.objects.filter(user_type='staff'), - widget=forms.CheckboxSelectMultiple, - required=False, - label=_("Select Users")) +# ) +# system_users=forms.ModelMultipleChoiceField( +# queryset=User.objects.filter(user_type='staff'), +# widget=forms.CheckboxSelectMultiple, +# required=False, +# label=_("Select Users")) - class Meta: - model = InterviewSchedule - fields = ['participants','system_users'] +# class Meta: +# model = BulkInterviewTemplate +# fields = ['participants','system_users'] @@ -1973,203 +1875,203 @@ class InterviewParticpantsForm(forms.ModelForm): # # } #during bulk schedule -class OnsiteLocationForm(forms.ModelForm): - class Meta: - model = OnsiteLocationDetails - # Include 'room_number' and update the field list - fields = ['topic', 'physical_address', 'room_number'] - widgets = { - 'topic': forms.TextInput( - attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'} - ), +# class OnsiteLocationForm(forms.ModelForm): +# class Meta: +# model = OnsiteLocationDetails +# # Include 'room_number' and update the field list +# fields = ['topic', 'physical_address', 'room_number'] +# widgets = { +# 'topic': forms.TextInput( +# attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'} +# ), - 'physical_address': forms.TextInput( - attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'} - ), +# 'physical_address': forms.TextInput( +# attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'} +# ), - 'room_number': forms.TextInput( - attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'} - ), +# 'room_number': forms.TextInput( +# attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'} +# ), - } +# } -class InterviewEmailForm(forms.Form): - subject = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'form-control'})) - message_for_candidate = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) - message_for_agency = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}),required=False) - message_for_participants = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) +# class InterviewEmailForm(forms.Form): +# subject = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'form-control'})) +# message_for_candidate = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) +# message_for_agency = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}),required=False) +# message_for_participants = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) - def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs): - """ - meeting: an InterviewLocation instance (e.g., ZoomMeetingDetails or OnsiteLocationDetails) - """ - super().__init__(*args, **kwargs) +# def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs): +# """ +# meeting: an InterviewLocation instance (e.g., ZoomMeetingDetails or OnsiteLocationDetails) +# """ +# super().__init__(*args, **kwargs) - # ✅ meeting is already the InterviewLocation — do NOT use .interview_location - location = meeting +# # ✅ meeting is already the InterviewLocation — do NOT use .interview_location +# location = meeting - # --- Determine concrete details (Zoom or Onsite) --- - if location.location_type == location.LocationType.REMOTE: - details = getattr(location, 'zoommeetingdetails', None) - elif location.location_type == location.LocationType.ONSITE: - details = getattr(location, 'onsitelocationdetails', None) - else: - details = None +# # --- Determine concrete details (Zoom or Onsite) --- +# if location.location_type == location.LocationType.REMOTE: +# details = getattr(location, 'zoommeetingdetails', None) +# elif location.location_type == location.LocationType.ONSITE: +# details = getattr(location, 'onsitelocationdetails', None) +# else: +# details = None - # --- Extract meeting info safely --- - if details and details.start_time: - formatted_date = details.start_time.strftime('%Y-%m-%d') - formatted_time = details.start_time.strftime('%I:%M %p') - duration = details.duration - meeting_link = location.details_url or "N/A (See Location Topic)" - else: - formatted_date = "TBD - Awaiting Scheduling" - formatted_time = "TBD" - duration = "N/A" - meeting_link = "Not Available" +# # --- Extract meeting info safely --- +# if details and details.start_time: +# formatted_date = details.start_time.strftime('%Y-%m-%d') +# formatted_time = details.start_time.strftime('%I:%M %p') +# duration = details.duration +# meeting_link = location.details_url or "N/A (See Location Topic)" +# else: +# formatted_date = "TBD - Awaiting Scheduling" +# formatted_time = "TBD" +# duration = "N/A" +# meeting_link = "Not Available" - job_title = job.title - agency_name = ( - candidate.hiring_agency.name - if candidate.belong_to_an_agency and candidate.hiring_agency - else "Hiring Agency" - ) +# job_title = job.title +# agency_name = ( +# candidate.hiring_agency.name +# if candidate.belong_to_an_agency and candidate.hiring_agency +# else "Hiring Agency" +# ) - # --- Participant names for internal email --- - external_names = ", ".join([p.name for p in external_participants]) - system_names = ", ".join([u.get_full_name() or u.username for u in system_participants]) - participant_names = ", ".join(filter(None, [external_names, system_names])) +# # --- Participant names for internal email --- +# external_names = ", ".join([p.name for p in external_participants]) +# system_names = ", ".join([u.get_full_name() or u.username for u in system_participants]) +# participant_names = ", ".join(filter(None, [external_names, system_names])) - # --- Candidate Message --- - candidate_message = f""" -Dear {candidate.full_name}, +# # --- Candidate Message --- +# candidate_message = f""" +# Dear {candidate.full_name}, -Thank you for your interest in the **{job_title}** position at KAAUH. We're pleased to invite you to an interview! +# Thank you for your interest in the **{job_title}** position at KAAUH. We're pleased to invite you to an interview! -The details of your interview are as follows: +# The details of your interview are as follows: -- **Date:** {formatted_date} -- **Time:** {formatted_time} (RIYADH TIME) -- **Duration:** {duration} minutes -- **Meeting Link/Location:** {meeting_link} +# - **Date:** {formatted_date} +# - **Time:** {formatted_time} (RIYADH TIME) +# - **Duration:** {duration} minutes +# - **Meeting Link/Location:** {meeting_link} -Please be ready at the scheduled time. +# Please be ready at the scheduled time. -Kindly reply to confirm your attendance or propose an alternative if needed. +# Kindly reply to confirm your attendance or propose an alternative if needed. -We look forward to meeting you. +# We look forward to meeting you. -Best regards, -KAAUH Hiring Team -""".strip() +# Best regards, +# KAAUH Hiring Team +# """.strip() - # --- Agency Message --- - agency_message = f""" -Dear {agency_name}, +# # --- Agency Message --- +# agency_message = f""" +# Dear {agency_name}, -This is to inform you that your candidate, **{candidate.full_name}**, has been scheduled for an interview for the **{job_title}** position. +# This is to inform you that your candidate, **{candidate.full_name}**, has been scheduled for an interview for the **{job_title}** position. -**Interview Details:** -- **Date:** {formatted_date} -- **Time:** {formatted_time} (RIYADH TIME) -- **Duration:** {duration} minutes -- **Meeting Link/Location:** {meeting_link} +# **Interview Details:** +# - **Date:** {formatted_date} +# - **Time:** {formatted_time} (RIYADH TIME) +# - **Duration:** {duration} minutes +# - **Meeting Link/Location:** {meeting_link} -Please ensure the candidate is informed and prepared. +# Please ensure the candidate is informed and prepared. -Best regards, -KAAUH Hiring Team -""".strip() +# Best regards, +# KAAUH Hiring Team +# """.strip() - # --- Participants (Interview Panel) Message --- - participants_message = f""" -Hi Team, +# # --- Participants (Interview Panel) Message --- +# participants_message = f""" +# Hi Team, -You are scheduled to interview **{candidate.full_name}** for the **{job_title}** role. +# You are scheduled to interview **{candidate.full_name}** for the **{job_title}** role. -**Interview Summary:** -- **Candidate:** {candidate.full_name} -- **Date:** {formatted_date} -- **Time:** {formatted_time} (RIYADH TIME) -- **Duration:** {duration} minutes -- **Location/Link:** {meeting_link} -- **Fellow Interviewers:** {participant_names} +# **Interview Summary:** +# - **Candidate:** {candidate.full_name} +# - **Date:** {formatted_date} +# - **Time:** {formatted_time} (RIYADH TIME) +# - **Duration:** {duration} minutes +# - **Location/Link:** {meeting_link} +# - **Fellow Interviewers:** {participant_names} -**Action Items:** -1. Review the candidate’s resume and application notes. -2. Join via the link above (or be at the physical location) on time. -3. Coordinate among yourselves for role coverage. +# **Action Items:** +# 1. Review the candidate’s resume and application notes. +# 2. Join via the link above (or be at the physical location) on time. +# 3. Coordinate among yourselves for role coverage. -Thank you! -""".strip() +# Thank you! +# """.strip() - # --- Set initial values --- - self.initial.update({ - 'subject': f"Interview Invitation: {job_title} - {candidate.full_name}", - 'message_for_candidate': candidate_message, - 'message_for_agency': agency_message, - 'message_for_participants': participants_message, - }) +# # --- Set initial values --- +# self.initial.update({ +# 'subject': f"Interview Invitation: {job_title} - {candidate.full_name}", +# 'message_for_candidate': candidate_message, +# 'message_for_agency': agency_message, +# 'message_for_participants': participants_message, +# }) -class OnsiteReshuduleForm(forms.ModelForm): - class Meta: - model = OnsiteLocationDetails - fields = ['topic', 'physical_address', 'room_number','start_time','duration','status'] - widgets = { - 'topic': forms.TextInput( - attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'} - ), +# class OnsiteReshuduleForm(forms.ModelForm): +# class Meta: +# model = OnsiteLocationDetails +# fields = ['topic', 'physical_address', 'room_number','start_time','duration','status'] +# widgets = { +# 'topic': forms.TextInput( +# attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'} +# ), - 'physical_address': forms.TextInput( - attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'} - ), +# 'physical_address': forms.TextInput( +# attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'} +# ), - 'room_number': forms.TextInput( - attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'} - ), +# 'room_number': forms.TextInput( +# attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'} +# ), - } +# } -class OnsiteScheduleForm(forms.ModelForm): - # Add fields for the foreign keys required by ScheduledInterview - application = forms.ModelChoiceField( - queryset=Application.objects.all(), - widget=forms.HiddenInput(), # Hide this in the form, set by the view - label=_("Candidate Application") - ) - job = forms.ModelChoiceField( - queryset=JobPosting.objects.all(), - widget=forms.HiddenInput(), # Hide this in the form, set by the view - label=_("Job Posting") - ) +# class OnsiteScheduleForm(forms.ModelForm): +# # Add fields for the foreign keys required by ScheduledInterview +# application = forms.ModelChoiceField( +# queryset=Application.objects.all(), +# widget=forms.HiddenInput(), # Hide this in the form, set by the view +# label=_("Candidate Application") +# ) +# job = forms.ModelChoiceField( +# queryset=JobPosting.objects.all(), +# widget=forms.HiddenInput(), # Hide this in the form, set by the view +# label=_("Job Posting") +# ) - class Meta: - model = OnsiteLocationDetails - # Include all fields from OnsiteLocationDetails plus the new ones - fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job'] +# class Meta: +# model = OnsiteLocationDetails +# # Include all fields from OnsiteLocationDetails plus the new ones +# fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job'] - widgets = { - 'topic': forms.TextInput( - attrs={'placeholder': _('Enter the Meeting Topic'), 'class': 'form-control'} - ), - 'physical_address': forms.TextInput( - attrs={'placeholder': _('Physical address (e.g., street address)'), 'class': 'form-control'} - ), - 'room_number': forms.TextInput( - attrs={'placeholder': _('Room Number/Name (Optional)'), 'class': 'form-control'} - ), - # You should explicitly set widgets for start_time, duration, and status here - # if they need Bootstrap classes, otherwise they will use default HTML inputs. - # Example: - 'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), - 'duration': forms.NumberInput(attrs={'class': 'form-control', 'min': 15}), - 'status': forms.HiddenInput(), # Status should default to SCHEDULED, so hide it. - } +# widgets = { +# 'topic': forms.TextInput( +# attrs={'placeholder': _('Enter the Meeting Topic'), 'class': 'form-control'} +# ), +# 'physical_address': forms.TextInput( +# attrs={'placeholder': _('Physical address (e.g., street address)'), 'class': 'form-control'} +# ), +# 'room_number': forms.TextInput( +# attrs={'placeholder': _('Room Number/Name (Optional)'), 'class': 'form-control'} +# ), +# # You should explicitly set widgets for start_time, duration, and status here +# # if they need Bootstrap classes, otherwise they will use default HTML inputs. +# # Example: +# 'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), +# 'duration': forms.NumberInput(attrs={'class': 'form-control', 'min': 15}), +# 'status': forms.HiddenInput(), # Status should default to SCHEDULED, so hide it. +# } @@ -2504,9 +2406,384 @@ class StaffAssignmentForm(forms.ModelForm): ) def clean_assigned_to(self): - """Validate the assigned staff member""" + """Validate that assigned user is a staff member""" assigned_to = self.cleaned_data.get('assigned_to') if assigned_to and assigned_to.user_type != 'staff': raise forms.ValidationError(_('Only staff members can be assigned to jobs.')) return assigned_to + +class RemoteInterviewForm(forms.Form): + """Form for creating remote interviews""" + + # Add Interview model fields to the form + topic = forms.CharField( + max_length=255, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'e.g., Software Interview' + }), + label=_('Meeting Topic') + ) + + interview_date = forms.DateField( + required=False, + widget=forms.DateInput(attrs={ + 'class': 'form-control', + 'type': 'date' + }), + label=_('Interview Date') + ) + + interview_time = forms.TimeField( + required=False, + widget=forms.TimeInput(attrs={ + 'class': 'form-control', + 'type': 'time' + }), + label=_('Interview Time') + ) + + duration = forms.IntegerField( + min_value=1, + required=False, + widget=forms.NumberInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Duration in minutes' + }), + label=_('Duration (minutes)') + ) + + + # class Meta: + # model = ScheduledInterview + # fields = ['topic','interview_date', 'interview_time'] + # widgets = { + # # 'application': forms.Select(attrs={'class': 'form-control', 'required': True}), + # 'interview_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'required': True}), + # 'interview_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time', 'required': True}), + # # 'participants': forms.SelectMultiple(attrs={'class': 'form-control select2'}), + # # 'system_users': forms.SelectMultiple(attrs={'class': 'form-control select2'}), + # # 'status': forms.Select(attrs={'class': 'form-control'}), + # } + # labels = { + # # 'application': _('Candidate'), + # 'interview_date': _('Interview Date'), + # 'interview_time': _('Interview Time'), + # 'participants': _('External Participants'), + # 'system_users': _('System Users'), + # 'status': _('Status'), + # } + + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + # # Filter applications to only show candidates in Interview stage + # self.fields['application'].queryset = Application.objects.filter(stage='Interview').order_by('-created_at') + + # # Filter participants and system users + # self.fields['participants'].queryset = Participants.objects.all().order_by('name') + # self.fields['system_users'].queryset = User.objects.filter(user_type='staff').order_by('first_name', 'last_name') + + # self.helper = FormHelper() + # self.helper.form_method = 'post' + # self.helper.form_class = 'g-3' + + # self.helper.layout = Layout( + # Field('application', css_class='form-control'), + # Row( + # Column('interview_date', css_class='col-md-6'), + # Column('interview_time', css_class='col-md-6'), + # css_class='g-3 mb-3', + # ), + # Row( + # Column('participants', css_class='col-md-6'), + # Column('system_users', css_class='col-md-6'), + # css_class='g-3 mb-3', + # ), + # Field('status', css_class='form-control'), + # Div( + # Field('topic', css_class='form-control'), + # Field('details_url', css_class='form-control'), + # Field('meeting_id', css_class='form-control'), + # Field('password', css_class='form-control'), + # Field('duration', css_class='form-control'), + # css_class='mb-4' + # ), + # Div( + # Submit('submit', _('Schedule Remote Interview'), css_class='btn btn-primary'), + # css_class='col-12 mt-4', + # ), + # ) + + # def clean_interview_date(self): + # """Validate interview date is not in the past""" + # interview_date = self.cleaned_data.get('interview_date') + # if interview_date and interview_date < timezone.now().date(): + # raise forms.ValidationError(_('Interview date cannot be in the past.')) + # return interview_date + + # def clean_meeting_id(self): + # """Validate meeting ID is provided if URL is provided""" + # details_url = self.cleaned_data.get('details_url') + # meeting_id = self.cleaned_data.get('meeting_id') + + # # If a URL is provided, require a meeting ID as well + # if details_url and not meeting_id: + # raise forms.ValidationError(_('Meeting ID is required when providing a meeting URL.')) + + # return meeting_id + + # def clean_details_url(self): + # """Validate URL format""" + # details_url = self.cleaned_data.get('details_url') + # if details_url: + # validator = URLValidator() + # try: + # validator(details_url) + # except ValidationError: + # raise forms.ValidationError(_('Please enter a valid URL (e.g., https://zoom.us/j/...)')) + # return details_url + + # def clean_duration(self): + # """Validate duration is positive""" + # duration = self.cleaned_data.get('duration') + # if duration is not None and duration < 1: + # raise forms.ValidationError(_('Duration must be at least 1 minute.')) + # return duration or 60 # Default to 60 if not provided + + # def clean(self): + # """Custom validation for remote interview""" + # cleaned_data = super().clean() + # interview_date = cleaned_data.get('interview_date') + # interview_time = cleaned_data.get('interview_time') + # details_url = cleaned_data.get('details_url') + # meeting_id = cleaned_data.get('meeting_id') + + # # Validate interview date and time are not in the past + # if interview_date and interview_time: + # interview_datetime = timezone.make_aware( + # timezone.datetime.combine(interview_date, interview_time), + # timezone.get_current_timezone() + # ) + # if interview_datetime <= timezone.now(): + # raise forms.ValidationError(_('Interview date and time cannot be in the past.')) + + # # If both URL and meeting ID are provided, validate they are consistent + # if details_url and meeting_id: + # if meeting_id not in details_url: + # # This is optional - you can remove this validation if you don't want to enforce it + # pass # Just a warning that the two may not match + + # # Validate that for remote interviews, at least basic location info is provided + # topic = cleaned_data.get('topic') + # if not topic: + # # Allow empty topic but warn that it's recommended + # pass + + # return cleaned_data + + # def save(self, commit=True): + # """Override save to handle the related Interview instance""" + # instance = super().save(commit=False) + + # if commit: + # # Save the scheduled interview first + # instance.save() + + # # Create and save the related Interview instance with remote details + # from .models import Interview + # interview = Interview( + # topic=self.cleaned_data.get('topic', ''), + # details_url=self.cleaned_data.get('details_url', ''), + # meeting_id=self.cleaned_data.get('meeting_id', ''), + # password=self.cleaned_data.get('password', ''), + # duration=self.cleaned_data.get('duration', 60), + # location_type=Interview.LocationType.REMOTE, + # start_time=timezone.make_aware( + # timezone.datetime.combine( + # instance.interview_date, + # instance.interview_time + # ) + # ) + # ) + # interview.full_clean() # Validate the interview model + # interview.save() + + # # Link the interview to the scheduled interview + # instance.interview = interview + # instance.save() + + # return instance + + +class OnsiteInterviewForm(forms.Form): + """Form for creating onsite interviews""" + + # Add Interview model fields to the form + topic = forms.CharField( + max_length=255, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'e.g., In-person Interview' + }), + label=_('Interview Topic') + ) + physical_address = forms.CharField( + max_length=255, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Physical address' + }), + label=_('Physical Address') + ) + room_number = forms.CharField( + max_length=50, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Room number' + }), + label=_('Room Number') + ) + interview_date = forms.DateField( + widget=forms.DateInput(attrs={ + 'class': 'form-control', + 'type': 'date', + 'required': True + }), + label=_('Interview Date') + ) + interview_time = forms.TimeField( + widget=forms.TimeInput(attrs={ + 'class': 'form-control', + 'type': 'time', + 'required': True + }), + label=_('Interview Time') + ) + duration = forms.IntegerField( + min_value=1, + required=False, + widget=forms.NumberInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Duration in minutes' + }), + label=_('Duration (minutes)') + ) + + # class Meta: + # model = ScheduledInterview + # fields = ['application', 'interview_date', 'interview_time', 'participants', 'system_users', 'status'] + # widgets = { + # 'application': forms.Select(attrs={'class': 'form-control', 'required': True}), + # 'interview_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'required': True}), + # 'interview_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time', 'required': True}), + # 'participants': forms.SelectMultiple(attrs={'class': 'form-control select2'}), + # 'system_users': forms.SelectMultiple(attrs={'class': 'form-control select2'}), + # 'status': forms.Select(attrs={'class': 'form-control'}), + # } + # labels = { + # 'application': _('Application'), + # 'interview_date': _('Interview Date'), + # 'interview_time': _('Interview Time'), + # 'participants': _('External Participants'), + # 'system_users': _('System Users'), + # 'status': _('Status'), + # } + + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + # # Filter applications to only show candidates in Interview stage + # self.fields['application'].queryset = Application.objects.filter(stage='Interview').order_by('-created_at') + + # # Filter participants and system users + # self.fields['participants'].queryset = Participants.objects.all().order_by('name') + # self.fields['system_users'].queryset = User.objects.filter(user_type='staff').order_by('first_name', 'last_name') + + # self.helper = FormHelper() + # self.helper.form_method = 'post' + # self.helper.form_class = 'g-3' + + # self.helper.layout = Layout( + # Field('application', css_class='form-control'), + # Row( + # Column('interview_date', css_class='col-md-6'), + # Column('interview_time', css_class='col-md-6'), + # css_class='g-3 mb-3', + # ), + # Row( + # Column('participants', css_class='col-md-6'), + # Column('system_users', css_class='col-md-6'), + # css_class='g-3 mb-3', + # ), + # Field('status', css_class='form-control'), + # Div( + # Field('topic', css_class='form-control'), + # Field('physical_address', css_class='form-control'), + # Field('room_number', css_class='form-control'), + # Field('duration', css_class='form-control'), + # css_class='mb-4' + # ), + # Div( + # Submit('submit', _('Schedule Onsite Interview'), css_class='btn btn-primary'), + # css_class='col-12 mt-4', + # ), + # ) + + # def clean_interview_date(self): + # """Validate interview date is not in the past""" + # interview_date = self.cleaned_data.get('interview_date') + # if interview_date and interview_date < timezone.now().date(): + # raise forms.ValidationError(_('Interview date cannot be in the past.')) + # return interview_date + + # def clean(self): + # """Custom validation for onsite interview""" + # cleaned_data = super().clean() + # interview_date = cleaned_data.get('interview_date') + # interview_time = cleaned_data.get('interview_time') + + # if interview_date and interview_time: + # interview_datetime = timezone.make_aware( + # timezone.datetime.combine(interview_date, interview_time), + # timezone.get_current_timezone() + # ) + # if interview_datetime <= timezone.now(): + # raise forms.ValidationError(_('Interview date and time cannot be in the past.')) + + # return cleaned_data + + # def save(self, commit=True): + # """Override save to handle the related Interview instance""" + # instance = super().save(commit=False) + + # if commit: + # # Save the scheduled interview first + # instance.save() + + # # Create and save the related Interview instance with onsite details + # from .models import Interview + # interview = Interview( + # topic=self.cleaned_data.get('topic', ''), + # physical_address=self.cleaned_data.get('physical_address', ''), + # room_number=self.cleaned_data.get('room_number', ''), + # duration=self.cleaned_data.get('duration', 60), + # location_type=Interview.LocationType.ONSITE, + # start_time=timezone.make_aware( + # timezone.datetime.combine( + # instance.interview_date, + # instance.interview_time + # ) + # ) + # ) + # interview.full_clean() # Validate the interview model + # interview.save() + + # # Link the interview to the scheduled interview + # instance.interview = interview + # instance.save() + + # return instance diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index db362f9..d695f8e 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-11-17 09:52 +# Generated by Django 5.2.6 on 2025-11-26 11:13 import django.contrib.auth.models import django.contrib.auth.validators @@ -49,16 +49,29 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='InterviewLocation', + name='Interview', 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')), ('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')), + ('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')), ('details_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')), - ('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'", max_length=255, verbose_name='Location/Meeting Topic')), ('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')), + ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), + ('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), + ('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)), + ('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')), + ('password', models.CharField(blank=True, max_length=20, null=True)), + ('zoom_gateway_response', models.JSONField(blank=True, null=True)), + ('participant_video', models.BooleanField(default=True)), + ('join_before_host', models.BooleanField(default=False)), + ('host_email', models.CharField(blank=True, max_length=255, null=True)), + ('mute_upon_entry', models.BooleanField(default=False)), + ('waiting_room', models.BooleanField(default=False)), + ('physical_address', models.CharField(blank=True, max_length=255, null=True)), + ('room_number', models.CharField(blank=True, max_length=50, null=True)), ], options={ 'verbose_name': 'Interview Location', @@ -121,7 +134,6 @@ class Migration(migrations.Migration): ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), @@ -129,6 +141,7 @@ class Migration(migrations.Migration): ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), + ('email', models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], @@ -266,42 +279,22 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='OnsiteLocationDetails', + name='InterviewNote', fields=[ - ('interviewlocation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='recruitment.interviewlocation')), - ('physical_address', models.CharField(blank=True, max_length=255, null=True, verbose_name='Physical Address')), - ('room_number', models.CharField(blank=True, max_length=50, null=True, verbose_name='Room Number/Name')), - ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), - ('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), - ('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)), + ('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')), + ('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')), + ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')), + ('interview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')), ], options={ - 'verbose_name': 'Onsite Location Details', - 'verbose_name_plural': 'Onsite Location Details', + 'verbose_name': 'Interview Note', + 'verbose_name_plural': 'Interview Notes', + 'ordering': ['created_at'], }, - bases=('recruitment.interviewlocation',), - ), - migrations.CreateModel( - name='ZoomMeetingDetails', - fields=[ - ('interviewlocation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='recruitment.interviewlocation')), - ('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)), - ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), - ('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), - ('meeting_id', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='External Meeting ID')), - ('password', models.CharField(blank=True, max_length=20, null=True, verbose_name='Password')), - ('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')), - ('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')), - ('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')), - ('host_email', models.CharField(blank=True, null=True)), - ('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')), - ('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')), - ], - options={ - 'verbose_name': 'Zoom Meeting Details', - 'verbose_name_plural': 'Zoom Meeting Details', - }, - bases=('recruitment.interviewlocation',), ), migrations.CreateModel( name='JobPosting', @@ -312,8 +305,8 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('title', models.CharField(max_length=200)), ('department', models.CharField(blank=True, max_length=100)), - ('job_type', models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='FULL_TIME', max_length=20)), - ('workplace_type', models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='ON_SITE', max_length=20)), + ('job_type', models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='Full-time', max_length=20)), + ('workplace_type', models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20)), ('location_city', models.CharField(blank=True, max_length=100)), ('location_state', models.CharField(blank=True, max_length=100)), ('location_country', models.CharField(default='Saudia Arabia', max_length=100)), @@ -343,6 +336,8 @@ class Migration(migrations.Migration): ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('cancelled_at', models.DateTimeField(blank=True, null=True)), ('ai_parsed', models.BooleanField(default=False, help_text='Whether the job posting has been parsed by AI', verbose_name='AI Parsed')), + ('cv_zip_file', models.FileField(blank=True, null=True, upload_to='job_zips/')), + ('zip_created', models.BooleanField(default=False)), ('assigned_to', models.ForeignKey(blank=True, help_text='The user who has been assigned to this job', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Assigned To')), ('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), ('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')), @@ -353,14 +348,18 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), + migrations.AddField( + model_name='formtemplate', + name='job', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'), + ), migrations.CreateModel( - name='InterviewSchedule', + name='BulkInterviewTemplate', 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')), - ('schedule_interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], default='Remote', max_length=10, verbose_name='Interview Type')), ('start_date', models.DateField(db_index=True, verbose_name='Start Date')), ('end_date', models.DateField(db_index=True, verbose_name='End Date')), ('working_days', models.JSONField(verbose_name='Working Days')), @@ -372,18 +371,13 @@ class Migration(migrations.Migration): ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), ('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('template_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)')), + ('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')), ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')), ], options={ 'abstract': False, }, ), - migrations.AddField( - model_name='formtemplate', - name='job', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'), - ), migrations.AddField( model_name='application', name='job', @@ -474,7 +468,7 @@ class Migration(migrations.Migration): ('first_name', models.CharField(max_length=255, verbose_name='First Name')), ('last_name', models.CharField(max_length=255, verbose_name='Last Name')), ('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')), - ('email', models.EmailField(db_index=True, help_text='Unique email address for the person', max_length=254, unique=True, verbose_name='Email')), + ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')), ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), ('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')), ('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')), @@ -507,31 +501,13 @@ class Migration(migrations.Migration): ('interview_time', models.TimeField(verbose_name='Interview Time')), ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)), ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.application')), - ('interview_location', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='scheduled_interview', to='recruitment.interviewlocation', verbose_name='Meeting/Location Details')), + ('interview', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interview', verbose_name='Meeting/Location Details')), ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')), ('participants', models.ManyToManyField(blank=True, to='recruitment.participants')), - ('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interviews', to='recruitment.interviewschedule')), + ('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interviews', to='recruitment.bulkinterviewtemplate')), ('system_users', models.ManyToManyField(blank=True, related_name='attended_interviews', to=settings.AUTH_USER_MODEL)), ], ), - migrations.CreateModel( - name='InterviewNote', - 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')), - ('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')), - ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')), - ('interview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.scheduledinterview', verbose_name='Scheduled Interview')), - ], - options={ - 'verbose_name': 'Interview Note', - 'verbose_name_plural': 'Interview Notes', - 'ordering': ['created_at'], - }, - ), migrations.CreateModel( name='SharedFormTemplate', fields=[ @@ -656,11 +632,6 @@ class Migration(migrations.Migration): model_name='formsubmission', index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), ), - migrations.AddField( - model_name='notification', - name='related_meeting', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='recruitment.zoommeetingdetails', verbose_name='Related Meeting'), - ), migrations.AddIndex( model_name='formtemplate', index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), @@ -705,6 +676,14 @@ class Migration(migrations.Migration): model_name='message', index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'), ), + migrations.AddIndex( + model_name='notification', + index=models.Index(fields=['status', 'scheduled_for'], name='recruitment_status_0ebbe4_idx'), + ), + migrations.AddIndex( + model_name='notification', + index=models.Index(fields=['recipient'], name='recruitment_recipie_eadf4c_idx'), + ), migrations.AddIndex( model_name='person', index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'), @@ -757,12 +736,4 @@ class Migration(migrations.Migration): model_name='jobposting', index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), ), - migrations.AddIndex( - model_name='notification', - index=models.Index(fields=['status', 'scheduled_for'], name='recruitment_status_0ebbe4_idx'), - ), - migrations.AddIndex( - model_name='notification', - index=models.Index(fields=['recipient'], name='recruitment_recipie_eadf4c_idx'), - ), ] diff --git a/recruitment/migrations/0002_alter_jobposting_job_type_and_more.py b/recruitment/migrations/0002_alter_jobposting_job_type_and_more.py deleted file mode 100644 index c2e08a4..0000000 --- a/recruitment/migrations/0002_alter_jobposting_job_type_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-18 10:24 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='job_type', - field=models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='Full-time', max_length=20), - ), - migrations.AlterField( - model_name='jobposting', - name='workplace_type', - field=models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20), - ), - migrations.AlterField( - model_name='scheduledinterview', - name='interview_location', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interviewlocation', verbose_name='Meeting/Location Details'), - ), - ] diff --git a/recruitment/migrations/0002_scheduledinterview_interview_type.py b/recruitment/migrations/0002_scheduledinterview_interview_type.py new file mode 100644 index 0000000..645640a --- /dev/null +++ b/recruitment/migrations/0002_scheduledinterview_interview_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-11-26 12:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='scheduledinterview', + name='interview_type', + field=models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], default='Remote', max_length=20), + ), + ] diff --git a/recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py b/recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py deleted file mode 100644 index 9654118..0000000 --- a/recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-19 14:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_alter_jobposting_job_type_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='cv_zip_file', - field=models.FileField(blank=True, null=True, upload_to='job_zips/'), - ), - migrations.AddField( - model_name='jobposting', - name='zip_created', - field=models.BooleanField(default=False), - ), - ] diff --git a/recruitment/migrations/0004_alter_interviewschedule_template_location.py b/recruitment/migrations/0004_alter_interviewschedule_template_location.py deleted file mode 100644 index 13914f7..0000000 --- a/recruitment/migrations/0004_alter_interviewschedule_template_location.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-23 09:22 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_jobposting_cv_zip_file_jobposting_zip_created'), - ] - - operations = [ - migrations.AlterField( - model_name='interviewschedule', - name='template_location', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)'), - ), - ] diff --git a/recruitment/migrations/0005_alter_interviewschedule_template_location.py b/recruitment/migrations/0005_alter_interviewschedule_template_location.py deleted file mode 100644 index b7edfe0..0000000 --- a/recruitment/migrations/0005_alter_interviewschedule_template_location.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-23 09:41 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_alter_interviewschedule_template_location'), - ] - - operations = [ - migrations.AlterField( - model_name='interviewschedule', - name='template_location', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)'), - ), - ] diff --git a/recruitment/migrations/0006_alter_customuser_email.py b/recruitment/migrations/0006_alter_customuser_email.py deleted file mode 100644 index 49e496d..0000000 --- a/recruitment/migrations/0006_alter_customuser_email.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-23 12:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_alter_interviewschedule_template_location'), - ] - - operations = [ - migrations.AlterField( - model_name='customuser', - name='email', - field=models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True), - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index 991c205..a8975a2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -995,36 +995,36 @@ class Application(Base): """Legacy compatibility - get scheduled interviews for this application""" return self.scheduled_interviews.all() - @property - def get_latest_meeting(self): - """ - Retrieves the most specific location details (subclass instance) - of the latest ScheduledInterview for this application, or None. - """ - # 1. Get the latest ScheduledInterview - schedule = self.scheduled_interviews.order_by("-created_at").first() + # @property + # def get_latest_meeting(self): + # """ + # Retrieves the most specific location details (subclass instance) + # of the latest ScheduledInterview for this application, or None. + # """ + # # 1. Get the latest ScheduledInterview + # schedule = self.scheduled_interviews.order_by("-created_at").first() - # Check if a schedule exists and if it has an interview location - if not schedule or not schedule.interview_location: - return None + # # Check if a schedule exists and if it has an interview location + # if not schedule or not schedule.interview_location: + # return None - # Get the base location instance - interview_location = schedule.interview_location + # # Get the base location instance + # interview_location = schedule.interview_location - # 2. Safely retrieve the specific subclass details + # # 2. Safely retrieve the specific subclass details - # Determine the expected subclass accessor name based on the location_type - if interview_location.location_type == 'Remote': - accessor_name = 'zoommeetingdetails' - else: # Assumes 'Onsite' or any other type defaults to Onsite - accessor_name = 'onsitelocationdetails' + # # Determine the expected subclass accessor name based on the location_type + # if interview_location.location_type == 'Remote': + # accessor_name = 'zoommeetingdetails' + # else: # Assumes 'Onsite' or any other type defaults to Onsite + # accessor_name = 'onsitelocationdetails' - # Use getattr to safely retrieve the specific meeting object (subclass instance). - # If the accessor exists but points to None (because the subclass record was deleted), - # or if the accessor name is wrong for the object's true type, it will return None. - meeting_details = getattr(interview_location, accessor_name, None) + # # Use getattr to safely retrieve the specific meeting object (subclass instance). + # # If the accessor exists but points to None (because the subclass record was deleted), + # # or if the accessor name is wrong for the object's true type, it will return None. + # meeting_details = getattr(interview_location, accessor_name, None) - return meeting_details + # return meeting_details @property @@ -1094,9 +1094,6 @@ class Application(Base): - - - class TrainingMaterial(Base): title = models.CharField(max_length=255, verbose_name=_("Title")) content = CKEditor5Field( @@ -1118,17 +1115,155 @@ class TrainingMaterial(Base): return self.title -class InterviewLocation(Base): - """ - Base model for all interview location/meeting details (remote or onsite) - using Multi-Table Inheritance. - """ +# class InterviewLocation(Base): +# """ +# Base model for all interview location/meeting details (remote or onsite) +# using Multi-Table Inheritance. +# """ +# class LocationType(models.TextChoices): +# REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)') +# ONSITE = 'Onsite', _('In-Person (Physical Location)') + +# class Status(models.TextChoices): +# """Defines the possible real-time statuses for any interview location/meeting.""" +# WAITING = "waiting", _("Waiting") +# STARTED = "started", _("Started") +# ENDED = "ended", _("Ended") +# CANCELLED = "cancelled", _("Cancelled") + +# location_type = models.CharField( +# max_length=10, +# choices=LocationType.choices, +# verbose_name=_("Location Type"), +# db_index=True +# ) + +# details_url = models.URLField( +# verbose_name=_("Meeting/Location URL"), +# max_length=2048, +# blank=True, +# null=True +# ) + +# topic = models.CharField( # Renamed from 'description' to 'topic' to match your input +# max_length=255, +# verbose_name=_("Location/Meeting Topic"), +# blank=True, +# help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'") +# ) + +# timezone = models.CharField( +# max_length=50, +# verbose_name=_("Timezone"), +# default='UTC' +# ) + +# def __str__(self): +# # Use 'topic' instead of 'description' +# return f"{self.get_location_type_display()} - {self.topic[:50]}" + +# class Meta: +# verbose_name = _("Interview Location") +# verbose_name_plural = _("Interview Locations") + + +# class ZoomMeetingDetails(InterviewLocation): +# """Concrete model for remote interviews (Zoom specifics).""" + +# status = models.CharField( +# db_index=True, +# max_length=20, +# choices=InterviewLocation.Status.choices, +# default=InterviewLocation.Status.WAITING, +# ) +# start_time = models.DateTimeField( +# db_index=True, verbose_name=_("Start Time") +# ) +# duration = models.PositiveIntegerField( +# verbose_name=_("Duration (minutes)") +# ) +# meeting_id = models.CharField( +# db_index=True, +# max_length=50, +# unique=True, +# verbose_name=_("External Meeting ID") +# ) +# password = models.CharField( +# max_length=20, blank=True, null=True, verbose_name=_("Password") +# ) +# zoom_gateway_response = models.JSONField( +# blank=True, null=True, verbose_name=_("Zoom Gateway Response") +# ) +# participant_video = models.BooleanField( +# default=True, verbose_name=_("Participant Video") +# ) +# join_before_host = models.BooleanField( +# default=False, verbose_name=_("Join Before Host") +# ) + +# host_email=models.CharField(null=True,blank=True) +# mute_upon_entry = models.BooleanField( +# default=False, verbose_name=_("Mute Upon Entry") +# ) +# waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room")) + +# # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation *** +# # @classmethod +# # def create(cls, **kwargs): +# # """Factory method to ensure location_type is set to REMOTE.""" +# # return cls(location_type=InterviewLocation.LocationType.REMOTE, **kwargs) + +# class Meta: +# verbose_name = _("Zoom Meeting Details") +# verbose_name_plural = _("Zoom Meeting Details") + + +# class OnsiteLocationDetails(InterviewLocation): +# """Concrete model for onsite interviews (Room/Address specifics).""" + +# physical_address = models.CharField( +# max_length=255, +# verbose_name=_("Physical Address"), +# blank=True, +# null=True +# ) +# room_number = models.CharField( +# max_length=50, +# verbose_name=_("Room Number/Name"), +# blank=True, +# null=True +# ) +# start_time = models.DateTimeField( +# db_index=True, verbose_name=_("Start Time") +# ) +# duration = models.PositiveIntegerField( +# verbose_name=_("Duration (minutes)") +# ) +# status = models.CharField( +# db_index=True, +# max_length=20, +# choices=InterviewLocation.Status.choices, +# default=InterviewLocation.Status.WAITING, +# ) + +# # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation *** +# # @classmethod +# # def create(cls, **kwargs): +# # """Factory method to ensure location_type is set to ONSITE.""" +# # return cls(location_type=InterviewLocation.LocationType.ONSITE, **kwargs) + +# class Meta: +# verbose_name = _("Onsite Location Details") +# verbose_name_plural = _("Onsite Location Details") + + + +class Interview(Base): class LocationType(models.TextChoices): REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)') ONSITE = 'Onsite', _('In-Person (Physical Location)') class Status(models.TextChoices): - """Defines the possible real-time statuses for any interview location/meeting.""" WAITING = "waiting", _("Waiting") STARTED = "started", _("Started") ENDED = "ended", _("Ended") @@ -1141,137 +1276,73 @@ class InterviewLocation(Base): db_index=True ) + # Common fields + topic = models.CharField( + max_length=255, + verbose_name=_("Meeting/Location Topic"), + blank=True, + help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'") + ) details_url = models.URLField( verbose_name=_("Meeting/Location URL"), max_length=2048, blank=True, null=True ) - - topic = models.CharField( # Renamed from 'description' to 'topic' to match your input - max_length=255, - verbose_name=_("Location/Meeting Topic"), - blank=True, - help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'") + timezone = models.CharField(max_length=50, verbose_name=_("Timezone"), default='UTC') + start_time = models.DateTimeField(db_index=True, verbose_name=_("Start Time")) + duration = models.PositiveIntegerField(verbose_name=_("Duration (minutes)")) + status = models.CharField( + max_length=20, + choices=Status.choices, + default=Status.WAITING, + db_index=True ) - timezone = models.CharField( - max_length=50, - verbose_name=_("Timezone"), - default='UTC' + # Remote-specific (nullable) + meeting_id = models.CharField( + max_length=50, unique=True, null=True, blank=True, verbose_name=_("External Meeting ID") ) + password = models.CharField(max_length=20, blank=True, null=True) + zoom_gateway_response = models.JSONField(blank=True, null=True) + participant_video = models.BooleanField(default=True) + join_before_host = models.BooleanField(default=False) + host_email = models.CharField(max_length=255, blank=True, null=True) + mute_upon_entry = models.BooleanField(default=False) + waiting_room = models.BooleanField(default=False) + + # Onsite-specific (nullable) + physical_address = models.CharField(max_length=255, blank=True, null=True) + room_number = models.CharField(max_length=50, blank=True, null=True) def __str__(self): - # Use 'topic' instead of 'description' return f"{self.get_location_type_display()} - {self.topic[:50]}" class Meta: verbose_name = _("Interview Location") verbose_name_plural = _("Interview Locations") - -class ZoomMeetingDetails(InterviewLocation): - """Concrete model for remote interviews (Zoom specifics).""" - - status = models.CharField( - db_index=True, - max_length=20, - choices=InterviewLocation.Status.choices, - default=InterviewLocation.Status.WAITING, - ) - start_time = models.DateTimeField( - db_index=True, verbose_name=_("Start Time") - ) - duration = models.PositiveIntegerField( - verbose_name=_("Duration (minutes)") - ) - meeting_id = models.CharField( - db_index=True, - max_length=50, - unique=True, - verbose_name=_("External Meeting ID") - ) - password = models.CharField( - max_length=20, blank=True, null=True, verbose_name=_("Password") - ) - zoom_gateway_response = models.JSONField( - blank=True, null=True, verbose_name=_("Zoom Gateway Response") - ) - participant_video = models.BooleanField( - default=True, verbose_name=_("Participant Video") - ) - join_before_host = models.BooleanField( - default=False, verbose_name=_("Join Before Host") - ) - - host_email=models.CharField(null=True,blank=True) - mute_upon_entry = models.BooleanField( - default=False, verbose_name=_("Mute Upon Entry") - ) - waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room")) - - # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation *** - # @classmethod - # def create(cls, **kwargs): - # """Factory method to ensure location_type is set to REMOTE.""" - # return cls(location_type=InterviewLocation.LocationType.REMOTE, **kwargs) - - class Meta: - verbose_name = _("Zoom Meeting Details") - verbose_name_plural = _("Zoom Meeting Details") - - -class OnsiteLocationDetails(InterviewLocation): - """Concrete model for onsite interviews (Room/Address specifics).""" - - physical_address = models.CharField( - max_length=255, - verbose_name=_("Physical Address"), - blank=True, - null=True - ) - room_number = models.CharField( - max_length=50, - verbose_name=_("Room Number/Name"), - blank=True, - null=True - ) - start_time = models.DateTimeField( - db_index=True, verbose_name=_("Start Time") - ) - duration = models.PositiveIntegerField( - verbose_name=_("Duration (minutes)") - ) - status = models.CharField( - db_index=True, - max_length=20, - choices=InterviewLocation.Status.choices, - default=InterviewLocation.Status.WAITING, - ) - - # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation *** - # @classmethod - # def create(cls, **kwargs): - # """Factory method to ensure location_type is set to ONSITE.""" - # return cls(location_type=InterviewLocation.LocationType.ONSITE, **kwargs) - - class Meta: - verbose_name = _("Onsite Location Details") - verbose_name_plural = _("Onsite Location Details") - - - + def clean(self): + # Optional: add validation + if self.location_type == self.LocationType.REMOTE: + if not self.details_url: + raise ValidationError(_("Remote interviews require a meeting URL.")) + if not self.meeting_id: + raise ValidationError(_("Meeting ID is required for remote interviews.")) + elif self.location_type == self.LocationType.ONSITE: + if not (self.physical_address or self.room_number): + raise ValidationError(_("Onsite interviews require at least an address or room.")) # --- 2. Scheduling Models --- -class InterviewSchedule(Base): +class BulkInterviewTemplate(Base): """Stores the TEMPLATE criteria for BULK interview generation.""" # We need a field to store the template location details linked to this bulk schedule. # This location object contains the generic Zoom/Onsite info to be cloned. - template_location = models.ForeignKey( - InterviewLocation, + interview = models.ForeignKey( + Interview, on_delete=models.SET_NULL, related_name="schedule_templates", null=True, @@ -1279,15 +1350,6 @@ class InterviewSchedule(Base): verbose_name=_("Location Template (Zoom/Onsite)") ) - # NOTE: schedule_interview_type field is needed in the form, - # but not on the model itself if we use template_location. - # If you want to keep it: - schedule_interview_type = models.CharField( - max_length=10, - choices=InterviewLocation.LocationType.choices, - verbose_name=_("Interview Type"), - default=InterviewLocation.LocationType.REMOTE - ) job = models.ForeignKey( JobPosting, @@ -1332,6 +1394,9 @@ class InterviewSchedule(Base): class ScheduledInterview(Base): """Stores individual scheduled interviews (whether bulk or individually created).""" + class InterviewTypeChoice(models.TextChoices): + REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)') + ONSITE = 'Onsite', _('In-Person (Physical Location)') class InterviewStatus(models.TextChoices): SCHEDULED = "scheduled", _("Scheduled") @@ -1353,19 +1418,19 @@ class ScheduledInterview(Base): ) # Links to the specific, individual location/meeting details for THIS interview - interview_location = models.OneToOneField( - InterviewLocation, + interview = models.OneToOneField( + Interview, on_delete=models.CASCADE, related_name="scheduled_interview", null=True, blank=True, db_index=True, - verbose_name=_("Meeting/Location Details") + verbose_name=_("Interview/Meeting") ) # Link back to the bulk schedule template (optional if individually created) schedule = models.ForeignKey( - InterviewSchedule, + BulkInterviewTemplate, on_delete=models.SET_NULL, related_name="interviews", null=True, @@ -1378,7 +1443,11 @@ class ScheduledInterview(Base): interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date")) interview_time = models.TimeField(verbose_name=_("Interview Time")) - + interview_type = models.CharField( + max_length=20, + choices=InterviewTypeChoice.choices, + default=InterviewTypeChoice.REMOTE + ) status = models.CharField( db_index=True, max_length=20, @@ -1420,7 +1489,7 @@ class InterviewNote(Base): 1 interview = models.ForeignKey( - ScheduledInterview, + Interview, on_delete=models.CASCADE, related_name="notes", verbose_name=_("Scheduled Interview"), @@ -2301,14 +2370,14 @@ class Notification(models.Model): default=Status.PENDING, verbose_name=_("Status"), ) - related_meeting = models.ForeignKey( - ZoomMeetingDetails, - on_delete=models.CASCADE, - related_name="notifications", - null=True, - blank=True, - verbose_name=_("Related Meeting"), - ) + # related_meeting = models.ForeignKey( + # ZoomMeetingDetails, + # on_delete=models.CASCADE, + # related_name="notifications", + # null=True, + # blank=True, + # verbose_name=_("Related Meeting"), + # ) scheduled_for = models.DateTimeField( verbose_name=_("Scheduled Send Time"), help_text=_("The date and time this notification is scheduled to be sent."), diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 2ec216b..4716133 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -12,7 +12,7 @@ from . linkedin_service import LinkedInService from django.shortcuts import get_object_or_404 from . models import JobPosting from django.utils import timezone -from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails,Message +from . models import ScheduledInterview,Interview,Message from django.contrib.auth import get_user_model User = get_user_model() # Add python-docx import for Word document processing @@ -679,20 +679,28 @@ def create_interview_and_meeting( Synchronous task for a single interview slot, dispatched by django-q. """ try: - candidate = Application.objects.get(pk=candidate_id) + application = Application.objects.get(pk=candidate_id) job = JobPosting.objects.get(pk=job_id) - schedule = InterviewSchedule.objects.get(pk=schedule_id) + schedule = ScheduledInterview.objects.get(pk=schedule_id) interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time)) - meeting_topic = f"Interview for {job.title} - {candidate.name}" + meeting_topic = f"Interview for {job.title} - {application.name}" # 1. External API Call (Slow) - + # "status": "success", + # "message": "Meeting created successfully.", + # "meeting_details": { + # "join_url": meeting_data['join_url'], + # "meeting_id": meeting_data['id'], + # "password": meeting_data['password'], + # "host_email": meeting_data['host_email'] + # }, + # "zoom_gateway_response": meeting_data + # } result = create_zoom_meeting(meeting_topic, interview_datetime, duration) if result["status"] == "success": - # 2. Database Writes (Slow) - zoom_meeting = ZoomMeetingDetails.objects.create( + interview = Interview.objects.create( topic=meeting_topic, start_time=interview_datetime, duration=duration, @@ -703,14 +711,31 @@ def create_interview_and_meeting( password=result["meeting_details"]["password"], location_type="Remote" ) - ScheduledInterview.objects.create( - application=candidate, - job=job, - interview_location=zoom_meeting, - schedule=schedule, - interview_date=slot_date, - interview_time=slot_time - ) + schedule.interviews = interview + schedule.status = "Remote" + + schedule.save() + + # 2. Database Writes (Slow) + # zoom_meeting = ZoomMeetingDetails.objects.create( + # topic=meeting_topic, + # start_time=interview_datetime, + # duration=duration, + # meeting_id=result["meeting_details"]["meeting_id"], + # details_url=result["meeting_details"]["join_url"], + # zoom_gateway_response=result["zoom_gateway_response"], + # host_email=result["meeting_details"]["host_email"], + # password=result["meeting_details"]["password"], + # location_type="Remote" + # ) + # ScheduledInterview.objects.create( + # application=candidate, + # job=job, + # interview_location=zoom_meeting, + # schedule=schedule, + # interview_date=slot_date, + # interview_time=slot_time + # ) # Log success or use Django-Q result system for monitoring logger.info(f"Successfully scheduled interview for {Application.name}") @@ -745,7 +770,7 @@ def handle_zoom_webhook_event(payload): try: # Use filter().first() to avoid exceptions if the meeting doesn't exist yet, # and to simplify the logic flow. - meeting_instance = ZoomMeetingDetails.objects.filter(meeting_id=meeting_id_zoom).first() + meeting_instance = ''#TODO:update #ZoomMeetingDetails.objects.filter(meeting_id=meeting_id_zoom).first() print(meeting_instance) # --- 1. Creation and Update Events --- if event_type == 'meeting.updated': diff --git a/recruitment/tests.py b/recruitment/tests.py index 43c615e..d75c635 100644 --- a/recruitment/tests.py +++ b/recruitment/tests.py @@ -11,12 +11,12 @@ User = get_user_model() from .models import ( JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField, - FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, + FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview, TrainingMaterial, Source, HiringAgency, MeetingComment ) from .forms import ( JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm, - CandidateStageForm, InterviewScheduleForm, CandidateSignupForm + CandidateStageForm, BulkInterviewTemplateForm, CandidateSignupForm ) from .views import ( ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view, @@ -304,7 +304,7 @@ class FormTests(BaseTestCase): self.assertTrue(form.is_valid()) def test_interview_schedule_form(self): - """Test InterviewScheduleForm""" + """Test BulkInterviewTemplateForm""" # Update candidate to Interview stage first self.candidate.stage = 'Interview' self.candidate.save() @@ -315,7 +315,7 @@ class FormTests(BaseTestCase): 'end_date': (timezone.now() + timedelta(days=7)).date(), 'working_days': [0, 1, 2, 3, 4], # Monday to Friday } - form = InterviewScheduleForm(slug=self.job.slug, data=form_data) + form = BulkInterviewTemplateForm(slug=self.job.slug, data=form_data) self.assertTrue(form.is_valid()) def test_candidate_signup_form_valid(self): diff --git a/recruitment/tests_advanced.py b/recruitment/tests_advanced.py index 16ee992..df4628b 100644 --- a/recruitment/tests_advanced.py +++ b/recruitment/tests_advanced.py @@ -24,13 +24,13 @@ from PIL import Image from .models import ( JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField, - FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, + FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview, TrainingMaterial, Source, HiringAgency, MeetingComment, JobPostingImage, BreakTime ) from .forms import ( JobPostingForm, ApplicationForm, ZoomMeetingForm, MeetingCommentForm, - ApplicationStageForm, InterviewScheduleForm, BreakTimeFormSet + ApplicationStageForm, BulkInterviewTemplateForm, BreakTimeFormSet ) from .views import ( ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view, @@ -228,7 +228,7 @@ class AdvancedModelTests(TestCase): 'break_end_time': '13:00' } - form = InterviewScheduleForm(slug=self.job.slug, data=schedule_data) + form = BulkInterviewTemplateForm(slug=self.job.slug, data=schedule_data) self.assertTrue(form.is_valid()) def test_field_response_data_types(self): @@ -625,7 +625,7 @@ class AdvancedFormTests(TestCase): def test_form_dependency_validation(self): """Test validation for dependent form fields""" - # Test InterviewScheduleForm with dependent fields + # Test BulkInterviewTemplateForm with dependent fields schedule_data = { 'candidates': [], # Empty for now 'start_date': '2025-01-15', @@ -637,7 +637,7 @@ class AdvancedFormTests(TestCase): 'buffer_time': '15' } - form = InterviewScheduleForm(slug=self.job.slug, data=schedule_data) + form = BulkInterviewTemplateForm(slug=self.job.slug, data=schedule_data) self.assertFalse(form.is_valid()) self.assertIn('end_date', form.errors) @@ -667,7 +667,7 @@ class AdvancedFormTests(TestCase): def test_dynamic_form_fields(self): """Test forms with dynamically populated fields""" - # Test InterviewScheduleForm with dynamic candidate queryset + # Test BulkInterviewTemplateForm with dynamic candidate queryset # Create applications in Interview stage applications = [] for i in range(3): @@ -684,7 +684,7 @@ class AdvancedFormTests(TestCase): applications.append(application) # Form should only show Interview stage applications - form = InterviewScheduleForm(slug=self.job.slug) + form = BulkInterviewTemplateForm(slug=self.job.slug) self.assertEqual(form.fields['candidates'].queryset.count(), 3) for application in applications: diff --git a/recruitment/urls.py b/recruitment/urls.py index 9ab1d9b..36fdf30 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -207,21 +207,21 @@ urlpatterns = [ ), - path( - "jobs///reschedule_meeting_for_application//", - views.reschedule_meeting_for_application, - name="reschedule_meeting_for_application", - ), + # path( + # "jobs///reschedule_meeting_for_application//", + # views.reschedule_meeting_for_application, + # name="reschedule_meeting_for_application", + # ), path( "jobs//update_application_exam_status/", views.update_application_exam_status, name="update_application_exam_status", ), - path( - "jobs//bulk_update_application_exam_status/", - views.bulk_update_application_exam_status, - name="bulk_update_application_exam_status", - ), + # path( + # "jobs//bulk_update_application_exam_status/", + # views.bulk_update_application_exam_status, + # name="bulk_update_application_exam_status", + # ), path( "htmx//application_criteria_view/", views.application_criteria_view_htmx, @@ -266,16 +266,16 @@ urlpatterns = [ # path('api/templates/save/', views.save_form_template, name='save_form_template'), # path('api/templates//', views.load_form_template, name='load_form_template'), # path('api/templates//delete/', views.delete_form_template, name='delete_form_template'), - path( - "jobs//calendar/", - views.interview_calendar_view, - name="interview_calendar", - ), - path( - "jobs//calendar/interview//", - views.interview_detail_view, - name="interview_detail", - ), + # path( + # "jobs//calendar/", + # views.interview_calendar_view, + # name="interview_calendar", + # ), + # path( + # "jobs//calendar/interview//", + # views.interview_detail_view, + # name="interview_detail", + # ), # users urls path("user/", views.user_detail, name="user_detail"), @@ -333,26 +333,26 @@ urlpatterns = [ name="copy_to_clipboard", ), # Meeting Comments URLs - path( - "meetings//comments/add/", - views.add_meeting_comment, - name="add_meeting_comment", - ), - path( - "meetings//comments//edit/", - views.edit_meeting_comment, - name="edit_meeting_comment", - ), - path( - "meetings//comments//delete/", - views.delete_meeting_comment, - name="delete_meeting_comment", - ), - path( - "meetings//set_meeting_application/", - views.set_meeting_application, - name="set_meeting_application", - ), + # path( + # "meetings//comments/add/", + # views.add_meeting_comment, + # name="add_meeting_comment", + # ), + # path( + # "meetings//comments//edit/", + # views.edit_meeting_comment, + # name="edit_meeting_comment", + # ), + # path( + # "meetings//comments//delete/", + # views.delete_meeting_comment, + # name="delete_meeting_comment", + # ), + # path( + # "meetings//set_meeting_application/", + # views.set_meeting_application, + # name="set_meeting_application", + # ), # Hiring Agency URLs path("agencies/", views.agency_list, name="agency_list"), path("agencies/create/", views.agency_create, name="agency_create"), @@ -510,31 +510,31 @@ urlpatterns = [ # path('notifications/mark-all-read/', views.notification_mark_all_read, name='notification_mark_all_read'), # path('api/notification-count/', views.api_notification_count, name='api_notification_count'), # participants urls - path( - "participants/", - views_frontend.ParticipantsListView.as_view(), - name="participants_list", - ), - path( - "participants/create/", - views_frontend.ParticipantsCreateView.as_view(), - name="participants_create", - ), - path( - "participants//", - views_frontend.ParticipantsDetailView.as_view(), - name="participants_detail", - ), - path( - "participants//update/", - views_frontend.ParticipantsUpdateView.as_view(), - name="participants_update", - ), - path( - "participants//delete/", - views_frontend.ParticipantsDeleteView.as_view(), - name="participants_delete", - ), + # path( + # "participants/", + # views_frontend.ParticipantsListView.as_view(), + # name="participants_list", + # ), + # path( + # "participants/create/", + # views_frontend.ParticipantsCreateView.as_view(), + # name="participants_create", + # ), + # path( + # "participants//", + # views_frontend.ParticipantsDetailView.as_view(), + # name="participants_detail", + # ), + # path( + # "participants//update/", + # views_frontend.ParticipantsUpdateView.as_view(), + # name="participants_update", + # ), + # path( + # "participants//delete/", + # views_frontend.ParticipantsDeleteView.as_view(), + # name="participants_delete", + # ), # Email composition URLs path( "jobs//applications/compose-email/", @@ -563,13 +563,23 @@ urlpatterns = [ path("application/documents//download/", views.document_download, name="application_document_download"), path('jobs//applications/compose_email/', views.compose_application_email, name='compose_application_email'), - path('interview/partcipants//',views.create_interview_participants,name='create_interview_participants'), - path('interview/email//',views.send_interview_email,name='send_interview_email'), + # path('interview/partcipants//',views.create_interview_participants,name='create_interview_participants'), + # path('interview/email//',views.send_interview_email,name='send_interview_email'), # Candidate Signup path('application/signup//', views.application_signup, name='application_signup'), # Password Reset path('user//password-reset/', views.portal_password_reset, name='portal_password_reset'), + # Interview URLs + path('interviews/', views.interview_list, name='interview_list'), + path('interviews//', views.interview_detail, name='interview_detail'), + + # Interview Creation URLs + path('interviews/create//', views.interview_create_type_selection, name='interview_create_type_selection'), + path('interviews/create//remote/', views.interview_create_remote, name='interview_create_remote'), + path('interviews/create//onsite/', views.interview_create_onsite, name='interview_create_onsite'), + path('interviews//get_interview_list', views.get_interview_list, name='get_interview_list'), + # # --- SCHEDULED INTERVIEW URLS (New Centralized Management) --- # path('interview/list/', views.interview_list, name='interview_list'), # path('interviews//', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'), @@ -577,64 +587,64 @@ urlpatterns = [ # path('interviews//delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'), #interview and meeting related urls - path( - "jobs//schedule-interviews/", - views.schedule_interviews_view, - name="schedule_interviews", - ), - path( - "jobs//confirm-schedule-interviews/", - views.confirm_schedule_interviews_view, - name="confirm_schedule_interviews_view", - ), + # path( + # "jobs//schedule-interviews/", + # views.schedule_interviews_view, + # name="schedule_interviews", + # ), + # path( + # "jobs//confirm-schedule-interviews/", + # views.confirm_schedule_interviews_view, + # name="confirm_schedule_interviews_view", + # ), - path( - "meetings/create-meeting/", - views.ZoomMeetingCreateView.as_view(), - name="create_meeting", - ), + # path( + # "meetings/create-meeting/", + # views.ZoomMeetingCreateView.as_view(), + # name="create_meeting", + # ), # path( # "meetings/meeting-details//", # views.ZoomMeetingDetailsView.as_view(), # name="meeting_details", # ), - path( - "meetings/update-meeting//", - views.ZoomMeetingUpdateView.as_view(), - name="update_meeting", - ), - path( - "meetings/delete-meeting//", - views.ZoomMeetingDeleteView, - name="delete_meeting", - ), + # path( + # "meetings/update-meeting//", + # views.ZoomMeetingUpdateView.as_view(), + # name="update_meeting", + # ), + # path( + # "meetings/delete-meeting//", + # views.ZoomMeetingDeleteView, + # name="delete_meeting", + # ), # Candidate Meeting Scheduling/Rescheduling URLs - path( - "jobs//applications//schedule-meeting/", - views.schedule_application_meeting, - name="schedule_application_meeting", - ), - path( - "api/jobs//applications//schedule-meeting/", - views.api_schedule_application_meeting, - name="api_schedule_application_meeting", - ), - path( - "jobs//applications//reschedule-meeting//", - views.reschedule_application_meeting, - name="reschedule_application_meeting", - ), - path( - "api/jobs//applications//reschedule-meeting//", - views.api_reschedule_application_meeting, - name="api_reschedule_application_meeting", - ), + # path( + # "jobs//applications//schedule-meeting/", + # views.schedule_application_meeting, + # name="schedule_application_meeting", + # ), + # path( + # "api/jobs//applications//schedule-meeting/", + # views.api_schedule_application_meeting, + # name="api_schedule_application_meeting", + # ), + # path( + # "jobs//applications//reschedule-meeting//", + # views.reschedule_application_meeting, + # name="reschedule_application_meeting", + # ), + # path( + # "api/jobs//applications//reschedule-meeting//", + # views.api_reschedule_application_meeting, + # name="api_reschedule_application_meeting", + # ), # New URL for simple page-based meeting scheduling - path( - "jobs//applications//schedule-meeting-page/", - views.schedule_meeting_for_application, - name="schedule_meeting_for_application", - ), + # path( + # "jobs//applications//schedule-meeting-page/", + # views.schedule_meeting_for_application, + # name="schedule_meeting_for_application", + # ), # path( # "jobs//applications//delete_meeting_for_application//", # views.delete_meeting_for_candidate, @@ -642,35 +652,35 @@ urlpatterns = [ # ), - path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"), + # path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"), # 1. Onsite Reschedule URL - path( - '/application//onsite/reschedule//', - views.reschedule_onsite_meeting, - name='reschedule_onsite_meeting' - ), + # path( + # '/application//onsite/reschedule//', + # views.reschedule_onsite_meeting, + # name='reschedule_onsite_meeting' + # ), # 2. Onsite Delete URL - path( - 'job//applications//delete-onsite-meeting//', - views.delete_onsite_meeting_for_application, - name='delete_onsite_meeting_for_application' - ), + # path( + # 'job//applications//delete-onsite-meeting//', + # views.delete_onsite_meeting_for_application, + # name='delete_onsite_meeting_for_application' + # ), - path( - 'job//application//schedule/onsite/', - views.schedule_onsite_meeting_for_application, - name='schedule_onsite_meeting_for_application' # This is the name used in the button - ), + # path( + # 'job//application//schedule/onsite/', + # views.schedule_onsite_meeting_for_application, + # name='schedule_onsite_meeting_for_application' # This is the name used in the button + # ), # Detail View (assuming slug is on ScheduledInterview) - path("interviews/meetings//", views.meeting_details, name="meeting_details"), + # path("interviews/meetings//", views.meeting_details, name="meeting_details"), # Email invitation URLs - path("interviews/meetings//send-application-invitation/", views.send_application_invitation, name="send_application_invitation"), - path("interviews/meetings//send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"), + # path("interviews/meetings//send-application-invitation/", views.send_application_invitation, name="send_application_invitation"), + # path("interviews/meetings//send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"), ] diff --git a/recruitment/views.py b/recruitment/views.py index 19a22a5..5db6769 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -26,12 +26,14 @@ from .forms import ( JobPostingStatusForm, LinkedPostContentForm, CandidateEmailForm, - InterviewForm, + # InterviewForm, ProfileImageUploadForm, - ParticipantsSelectForm, + # ParticipantsSelectForm, ApplicationForm, PasswordResetForm, StaffAssignmentForm, + RemoteInterviewForm, + OnsiteInterviewForm ) from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -60,12 +62,12 @@ from django.db.models.expressions import ExpressionWrapper from django.urls import reverse_lazy from django.db.models import Count, Avg, F, Q from .forms import ( - ZoomMeetingForm, + # ZoomMeetingForm, ApplicationExamDateForm, JobPostingForm, JobPostingImageForm, - InterviewNoteForm, - InterviewScheduleForm, + # InterviewNoteForm, + # BulkInterviewTemplateForm, FormTemplateForm, SourceForm, HiringAgencyForm, @@ -76,10 +78,10 @@ from .forms import ( PortalLoginForm, MessageForm, PersonForm, - OnsiteLocationForm, - OnsiteReshuduleForm, - OnsiteScheduleForm, - InterviewEmailForm + # OnsiteLocationForm, + # OnsiteReshuduleForm, + # OnsiteScheduleForm, + # InterviewEmailForm ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from rest_framework import viewsets @@ -113,9 +115,9 @@ from .models import ( FormField, FieldResponse, FormSubmission, - InterviewSchedule, - BreakTime, - ZoomMeetingDetails, + # BulkInterviewTemplate, + # BreakTime, + # ZoomMeetingDetails, Application, Person, JobPosting, @@ -129,9 +131,8 @@ from .models import ( Source, Message, Document, - InterviewLocation, - InterviewNote, - OnsiteLocationDetails + # InterviewLocation, + # InterviewNote, ) @@ -240,152 +241,152 @@ class CandidateViewSet(viewsets.ModelViewSet): serializer_class = ApplicationSerializer -class ZoomMeetingCreateView(StaffRequiredMixin, CreateView): - model = ZoomMeetingDetails - template_name = "meetings/create_meeting.html" - form_class = ZoomMeetingForm - success_url = "/" +# class ZoomMeetingCreateView(StaffRequiredMixin, CreateView): +# model = ZoomMeetingDetails +# template_name = "meetings/create_meeting.html" +# form_class = ZoomMeetingForm +# success_url = "/" - def form_valid(self, form): - instance = form.save(commit=False) - try: - topic = instance.topic - if instance.start_time < timezone.now(): - messages.error(self.request, "Start time must be in the future.") - return redirect( - reverse("create_meeting", kwargs={"slug": instance.slug}) - ) - start_time = instance.start_time - duration = instance.duration +# def form_valid(self, form): +# instance = form.save(commit=False) +# try: +# topic = instance.topic +# if instance.start_time < timezone.now(): +# messages.error(self.request, "Start time must be in the future.") +# return redirect( +# reverse("create_meeting", kwargs={"slug": instance.slug}) +# ) +# start_time = instance.start_time +# duration = instance.duration - result = create_zoom_meeting(topic, start_time, duration) - if result["status"] == "success": - instance.meeting_id = result["meeting_details"]["meeting_id"] - instance.join_url = result["meeting_details"]["join_url"] - instance.host_email = result["meeting_details"]["host_email"] - instance.password = result["meeting_details"]["password"] - instance.status = result["zoom_gateway_response"]["status"] - instance.zoom_gateway_response = result["zoom_gateway_response"] - instance.save() - messages.success(self.request, result["message"]) +# result = create_zoom_meeting(topic, start_time, duration) +# if result["status"] == "success": +# instance.meeting_id = result["meeting_details"]["meeting_id"] +# instance.join_url = result["meeting_details"]["join_url"] +# instance.host_email = result["meeting_details"]["host_email"] +# instance.password = result["meeting_details"]["password"] +# instance.status = result["zoom_gateway_response"]["status"] +# instance.zoom_gateway_response = result["zoom_gateway_response"] +# instance.save() +# messages.success(self.request, result["message"]) - return redirect(reverse("list_meetings")) - else: - messages.error(self.request, result["message"]) - return redirect( - reverse("create_meeting", kwargs={"slug": instance.slug}) - ) - except Exception as e: - messages.error(self.request, f"Error creating meeting: {e}") - return redirect(reverse("create_meeting", kwargs={"slug": instance.slug})) +# return redirect(reverse("list_meetings")) +# else: +# messages.error(self.request, result["message"]) +# return redirect( +# reverse("create_meeting", kwargs={"slug": instance.slug}) +# ) +# except Exception as e: +# messages.error(self.request, f"Error creating meeting: {e}") +# return redirect(reverse("create_meeting", kwargs={"slug": instance.slug})) -class ZoomMeetingDetailsView(StaffRequiredMixin, DetailView): - model = ZoomMeetingDetails - template_name = "meetings/meeting_details.html" - context_object_name = "meeting" +# class ZoomMeetingDetailsView(StaffRequiredMixin, DetailView): +# model = ZoomMeetingDetails +# template_name = "meetings/meeting_details.html" +# context_object_name = "meeting" - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - meeting = self.object - try: - interview = meeting.interview - except Exception as e: - print(e) - candidate = interview.candidate - job = meeting.get_job +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# meeting = self.object +# try: +# interview = meeting.interview +# except Exception as e: +# print(e) +# candidate = interview.candidate +# job = meeting.get_job - # Assuming interview.participants and interview.system_users hold the people: - participants = list(interview.participants.all()) + list( - interview.system_users.all() - ) - external_participants = list(interview.participants.all()) - system_participants = list(interview.system_users.all()) - total_participants = len(participants) - form = InterviewParticpantsForm(instance=interview) - context["form"] = form - context["email_form"] = InterviewEmailForm( - candidate=candidate, - external_participants=external_participants, - system_participants=system_participants, - meeting=meeting, - job=job, - ) - context["total_participants"] = total_participants - return context +# # Assuming interview.participants and interview.system_users hold the people: +# participants = list(interview.participants.all()) + list( +# interview.system_users.all() +# ) +# external_participants = list(interview.participants.all()) +# system_participants = list(interview.system_users.all()) +# total_participants = len(participants) +# form = InterviewParticpantsForm(instance=interview) +# context["form"] = form +# context["email_form"] = InterviewEmailForm( +# candidate=candidate, +# external_participants=external_participants, +# system_participants=system_participants, +# meeting=meeting, +# job=job, +# ) +# context["total_participants"] = total_participants +# return context -class ZoomMeetingUpdateView(StaffRequiredMixin, UpdateView): - model = ZoomMeetingDetails - form_class = ZoomMeetingForm - context_object_name = "meeting" - template_name = "meetings/update_meeting.html" - success_url = "/" +# class ZoomMeetingUpdateView(StaffRequiredMixin, UpdateView): +# model = ZoomMeetingDetails +# form_class = ZoomMeetingForm +# context_object_name = "meeting" +# template_name = "meetings/update_meeting.html" +# success_url = "/" - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - # Ensure the form is initialized with the instance's current values - if self.object: - kwargs['initial'] = getattr(kwargs, 'initial', {}) - initial_start_time = "" - if self.object.start_time: - try: - initial_start_time = self.object.start_time.strftime('%m-%d-%Y,T%H:%M') - except AttributeError: - print(f"Warning: start_time {self.object.start_time} is not a datetime object.") - initial_start_time = "" - kwargs['initial']['start_time'] = initial_start_time - return kwargs +# def get_form_kwargs(self): +# kwargs = super().get_form_kwargs() +# # Ensure the form is initialized with the instance's current values +# if self.object: +# kwargs['initial'] = getattr(kwargs, 'initial', {}) +# initial_start_time = "" +# if self.object.start_time: +# try: +# initial_start_time = self.object.start_time.strftime('%m-%d-%Y,T%H:%M') +# except AttributeError: +# print(f"Warning: start_time {self.object.start_time} is not a datetime object.") +# initial_start_time = "" +# kwargs['initial']['start_time'] = initial_start_time +# return kwargs - def form_valid(self, form): - instance = form.save(commit=False) - updated_data = { - "topic": instance.topic, - "start_time": instance.start_time.isoformat() + "Z", - "duration": instance.duration, - } - if instance.start_time < timezone.now(): - messages.error(self.request, "Start time must be in the future.") - return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) +# def form_valid(self, form): +# instance = form.save(commit=False) +# updated_data = { +# "topic": instance.topic, +# "start_time": instance.start_time.isoformat() + "Z", +# "duration": instance.duration, +# } +# if instance.start_time < timezone.now(): +# messages.error(self.request, "Start time must be in the future.") +# return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) - result = update_meeting(instance, updated_data) +# result = update_meeting(instance, updated_data) - if result["status"] == "success": - messages.success(self.request, result["message"]) - else: - messages.error(self.request, result["message"]) - return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) +# if result["status"] == "success": +# messages.success(self.request, result["message"]) +# else: +# messages.error(self.request, result["message"]) +# return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) -def ZoomMeetingDeleteView(request, slug): - meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) - if "HX-Request" in request.headers: - return render( - request, - "meetings/delete_meeting_form.html", - { - "meeting": meeting, - "delete_url": reverse("delete_meeting", kwargs={"slug": meeting.slug}), - }, - ) - if request.method == "POST": - try: - result = delete_zoom_meeting(meeting.meeting_id) - if ( - result["status"] == "success" - or "Meeting does not exist" in result["details"]["message"] - ): - meeting.delete() - messages.success(request, "Meeting deleted successfully.") - else: - messages.error( - request, f"{result['message']} , {result['details']['message']}" - ) - return redirect(reverse("list_meetings")) - except Exception as e: - messages.error(request, str(e)) - return redirect(reverse("list_meetings")) +# def ZoomMeetingDeleteView(request, slug): +# meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) +# if "HX-Request" in request.headers: +# return render( +# request, +# "meetings/delete_meeting_form.html", +# { +# "meeting": meeting, +# "delete_url": reverse("delete_meeting", kwargs={"slug": meeting.slug}), +# }, +# ) +# if request.method == "POST": +# try: +# result = delete_zoom_meeting(meeting.meeting_id) +# if ( +# result["status"] == "success" +# or "Meeting does not exist" in result["details"]["message"] +# ): +# meeting.delete() +# messages.success(request, "Meeting deleted successfully.") +# else: +# messages.error( +# request, f"{result['message']} , {result['details']['message']}" +# ) +# return redirect(reverse("list_meetings")) +# except Exception as e: +# messages.error(request, str(e)) +# return redirect(reverse("list_meetings")) # Job Posting # def job_list(request): @@ -528,17 +529,19 @@ def job_detail(request, slug): # --- 2. Quality Metrics (JSON Aggregation) --- + + applications_with_score = applications.filter(is_resume_parsed=True) total_applications_ = applications_with_score.count() # For context # Define the queryset for applications that have been parsed - score_expression = Cast( + score_expression = Cast( Coalesce( KeyTextTransform( - 'match_score', + 'match_score', KeyTransform('analysis_data_en', 'ai_analysis_data') ), - Value('0'), + Value('0'), ), output_field=IntegerField() ) @@ -547,7 +550,7 @@ def job_detail(request, slug): applications_with_score = applications_with_score.annotate( annotated_match_score=score_expression ) - + avg_match_score_result = applications_with_score.aggregate( avg_score=Avg('annotated_match_score') ) @@ -557,12 +560,6 @@ def job_detail(request, slug): annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD ).count() - high_potential_ratio = ( - round((high_potential_count / total_applications_) * 100, 1) - if total_applications_ > 0 - else 0 - ) - # --- 3. Time Metrics (Duration Aggregation) --- # Metric: Average Time from Applied to Interview (T2I) @@ -613,9 +610,7 @@ def job_detail(request, slug): .order_by("ai_analysis_data__analysis_data_en__category") ) # Prepare data for Chart.js - categories = [item["category"] for item in category_data] - applications_count = [item["application_count"] for item in category_data] # avg_scores = [round(item['avg_match_score'], 2) if item['avg_match_score'] is not None else 0 for item in category_data] @@ -825,11 +820,11 @@ def kaauh_career(request): selected_job_type = request.GET.get("employment_type", "") job_type_keys = active_jobs.order_by("job_type").distinct("job_type").values_list("job_type", flat=True) - + workplace_type_keys = active_jobs.order_by("workplace_type").distinct("workplace_type").values_list( "workplace_type", flat=True ).distinct() - + if selected_job_type and selected_job_type in job_type_keys: active_jobs = active_jobs.filter(job_type=selected_job_type) if selected_workplace_type and selected_workplace_type in workplace_type_keys: @@ -1440,324 +1435,323 @@ def form_submission_details(request, template_id, slug): ) -def _handle_get_request(request, slug, job): - """ - Handles GET requests, setting up forms and restoring candidate selections - from the session for persistence. - """ - SESSION_KEY = f"schedule_candidate_ids_{slug}" +# def _handle_get_request(request, slug, job): +# """ +# Handles GET requests, setting up forms and restoring candidate selections +# from the session for persistence. +# """ +# SESSION_KEY = f"schedule_candidate_ids_{slug}" - form = InterviewScheduleForm(slug=slug) - # break_formset = BreakTimeFormSet(prefix='breaktime') +# form = BulkInterviewTemplateForm(slug=slug) +# # break_formset = BreakTimeFormSet(prefix='breaktime') - selected_ids = [] +# selected_ids = [] - # 1. Capture IDs from HTMX request and store in session (when first clicked) - if "HX-Request" in request.headers: - candidate_ids = request.GET.getlist("candidate_ids") +# # 1. Capture IDs from HTMX request and store in session (when first clicked) +# if "HX-Request" in request.headers: +# candidate_ids = request.GET.getlist("candidate_ids") - if candidate_ids: - request.session[SESSION_KEY] = candidate_ids - selected_ids = candidate_ids +# if candidate_ids: +# request.session[SESSION_KEY] = candidate_ids +# selected_ids = candidate_ids - # 2. Restore IDs from session (on refresh or navigation) - if not selected_ids: - selected_ids = request.session.get(SESSION_KEY, []) +# # 2. Restore IDs from session (on refresh or navigation) +# if not selected_ids: +# selected_ids = request.session.get(SESSION_KEY, []) - # 3. Use the list of IDs to initialize the form - if selected_ids: - candidates_to_load = Application.objects.filter(pk__in=selected_ids) - print(candidates_to_load) - form.initial["applications"] = candidates_to_load +# # 3. Use the list of IDs to initialize the form +# if selected_ids: +# candidates_to_load = Application.objects.filter(pk__in=selected_ids) +# print(candidates_to_load) +# form.initial["applications"] = candidates_to_load - return render( - request, - "interviews/schedule_interviews.html", - {"form": form, "job": job}, - ) +# return render( +# request, +# "interviews/schedule_interviews.html", +# {"form": form, "job": job}, +# ) + + +#TODO:MAIN FUNCTIONS +# def _handle_preview_submission(request, slug, job): +# """ +# Handles the initial POST request (Preview Schedule). +# Validates forms, calculates slots, saves data to session, and renders preview. +# """ +# 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 +# applications = form.cleaned_data["applications"] +# start_date = form.cleaned_data["start_date"] +# end_date = form.cleaned_data["end_date"] +# working_days = form.cleaned_data["working_days"] +# start_time = form.cleaned_data["start_time"] +# end_time = form.cleaned_data["end_time"] +# interview_duration = form.cleaned_data["interview_duration"] +# buffer_time = form.cleaned_data["buffer_time"] +# break_start_time = form.cleaned_data["break_start_time"] +# break_end_time = form.cleaned_data["break_end_time"] +# schedule_interview_type=form.cleaned_data["schedule_interview_type"] +# # Process break times +# # breaks = [] +# # for break_form in break_formset: +# # print(break_form.cleaned_data) +# # if break_form.cleaned_data and not break_form.cleaned_data.get("DELETE"): +# # breaks.append( +# # { +# # "start_time": break_form.cleaned_data["start_time"].strftime("%H:%M:%S"), +# # "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"), +# # } +# # ) + +# # Create a temporary schedule object (not saved to DB) +# temp_schedule = BulkInterviewTemplate( +# job=job, +# start_date=start_date, +# end_date=end_date, +# working_days=working_days, +# start_time=start_time, +# end_time=end_time, +# interview_duration=interview_duration, +# buffer_time=buffer_time or 5, +# break_start_time=break_start_time or None, +# break_end_time=break_end_time or None, +# ) + +# # Get available slots (temp_breaks logic moved into get_available_time_slots if needed) +# available_slots = get_available_time_slots(temp_schedule) + +# if len(available_slots) < len(applications): +# messages.error( +# request, +# f"Not enough available slots. Required: {len(applications)}, Available: {len(available_slots)}", +# ) +# return render( +# request, +# "interviews/schedule_interviews.html", +# {"form": form, "job": job}, +# ) + +# # Create a preview schedule +# preview_schedule = [] +# for i, application in enumerate(applications): +# slot = available_slots[i] +# preview_schedule.append( +# {"application": application, "date": slot["date"], "time": slot["time"]} +# ) + +# # Save the form data to session for later use +# schedule_data = { +# "start_date": start_date.isoformat(), +# "end_date": end_date.isoformat(), +# "working_days": working_days, +# "start_time": start_time.isoformat(), +# "end_time": end_time.isoformat(), +# "interview_duration": interview_duration, +# "buffer_time": buffer_time, +# "break_start_time": break_start_time.isoformat() if break_start_time else None, +# "break_end_time": break_end_time.isoformat() if break_end_time else None, +# "candidate_ids": [c.id for c in applications], +# "schedule_interview_type":schedule_interview_type + +# } +# request.session[SESSION_DATA_KEY] = schedule_data + +# # Render the preview page +# return render( +# request, +# "interviews/preview_schedule.html", +# { +# "job": job, +# "schedule": preview_schedule, +# "start_date": start_date, +# "end_date": end_date, +# "working_days": working_days, +# "start_time": start_time, +# "end_time": end_time, +# "break_start_time": break_start_time, +# "break_end_time": break_end_time, +# "interview_duration": interview_duration, +# "buffer_time": buffer_time, +# "schedule_interview_type":schedule_interview_type, +# "form":OnsiteLocationForm() +# }, +# ) +# else: +# # Re-render the form if validation fails +# return render( +# request, +# "interviews/schedule_interviews.html", +# {"form": form, "job": job}, +# ) + + +# def _handle_confirm_schedule(request, slug, job): +# """ +# Handles the final POST request (Confirm Schedule). +# Creates the main schedule record and queues individual interviews asynchronously. +# """ + +# SESSION_DATA_KEY = "interview_schedule_data" +# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}" + +# # 1. Get schedule data from session +# schedule_data = request.session.get(SESSION_DATA_KEY) + +# if not schedule_data: +# messages.error(request, "Session expired. Please try again.") +# return redirect("schedule_interviews", slug=slug) + +# # 2. Create the Interview Schedule (Parent Record) +# try: +# # Handle break times: If they exist, convert them; otherwise, pass None. +# break_start = schedule_data.get("break_start_time") +# break_end = schedule_data.get("break_end_time") + +# schedule = BulkInterviewTemplate.objects.create( +# job=job, +# created_by=request.user, +# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(), +# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(), +# working_days=schedule_data["working_days"], +# start_time=time.fromisoformat(schedule_data["start_time"]), +# end_time=time.fromisoformat(schedule_data["end_time"]), +# interview_duration=schedule_data["interview_duration"], +# buffer_time=schedule_data["buffer_time"], +# # Convert time strings to time objects only if they exist and handle None gracefully +# break_start_time=time.fromisoformat(break_start) if break_start else None, +# break_end_time=time.fromisoformat(break_end) if break_end else None, +# schedule_interview_type=schedule_data.get("schedule_interview_type") +# ) +# except Exception as e: +# # Clear data on failure to prevent stale data causing repeated errors +# messages.error(request, f"Error creating schedule: {e}") +# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY] +# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY] +# return redirect("schedule_interviews", slug=slug) + +# # 3. Setup candidates and get slots +# candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"]) +# schedule.applications.set(candidates) +# available_slots = get_available_time_slots(schedule) + +# # 4. Handle Remote/Onsite logic +# if schedule_data.get("schedule_interview_type") == 'Remote': +# # ... (Remote logic remains unchanged) +# queued_count = 0 +# for i, candidate in enumerate(candidates): +# if i < len(available_slots): +# slot = available_slots[i] + +# async_task( +# "recruitment.tasks.create_interview_and_meeting", +# candidate.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration, +# ) +# queued_count += 1 + +# messages.success( +# request, +# f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!", +# ) + +# 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("job_detail", slug=slug) + +# elif schedule_data.get("schedule_interview_type") == 'Onsite': +# print("inside...") + +# if request.method == 'POST': +# form = OnsiteLocationForm(request.POST) + +# if form.is_valid(): + +# if not available_slots: +# messages.error(request, "No available slots found for the selected schedule range.") +# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) + +# # Extract common location data from the form +# physical_address = form.cleaned_data['physical_address'] +# room_number = form.cleaned_data['room_number'] +# topic=form.cleaned_data['topic'] + + +# try: +# # 1. Iterate over candidates and create a NEW Location object for EACH +# for i, candidate in enumerate(candidates): +# if i < len(available_slots): +# slot = available_slots[i] + + +# location_start_dt = datetime.combine(slot['date'], schedule.start_time) + +# # --- CORE FIX: Create a NEW Location object inside the loop --- +# onsite_location = OnsiteLocationDetails.objects.create( +# start_time=location_start_dt, +# duration=schedule.interview_duration, +# physical_address=physical_address, +# room_number=room_number, +# location_type="Onsite", +# topic=topic + +# ) + +# # 2. Create the ScheduledInterview, linking the unique location +# ScheduledInterview.objects.create( +# application=candidate, +# job=job, +# schedule=schedule, +# interview_date=slot['date'], +# interview_time=slot['time'], +# interview_location=onsite_location, +# ) + +# messages.success( +# request, +# f"Onsite schedule interviews created successfully for {len(candidates)} candidates." +# ) + +# # 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('job_detail', slug=job.slug) + +# except Exception as e: +# messages.error(request, f"Error creating onsite location/interviews: {e}") +# # On failure, re-render the form with the error and ensure 'job' is present +# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) + +# else: +# # Form is invalid, re-render with errors +# # Ensure 'job' is passed to prevent NoReverseMatch +# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) + +# else: +# # For a GET request +# form = OnsiteLocationForm() + +# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) -def _handle_preview_submission(request, slug, job): - """ - Handles the initial POST request (Preview Schedule). - Validates forms, calculates slots, saves data to session, and renders preview. - """ - SESSION_DATA_KEY = "interview_schedule_data" - form = InterviewScheduleForm(slug, request.POST) - # break_formset = BreakTimeFormSet(request.POST,prefix='breaktime') - - if form.is_valid(): - # Get the form data - applications = form.cleaned_data["applications"] - start_date = form.cleaned_data["start_date"] - end_date = form.cleaned_data["end_date"] - working_days = form.cleaned_data["working_days"] - start_time = form.cleaned_data["start_time"] - end_time = form.cleaned_data["end_time"] - interview_duration = form.cleaned_data["interview_duration"] - buffer_time = form.cleaned_data["buffer_time"] - break_start_time = form.cleaned_data["break_start_time"] - break_end_time = form.cleaned_data["break_end_time"] - schedule_interview_type=form.cleaned_data["schedule_interview_type"] - # Process break times - # breaks = [] - # for break_form in break_formset: - # print(break_form.cleaned_data) - # if break_form.cleaned_data and not break_form.cleaned_data.get("DELETE"): - # breaks.append( - # { - # "start_time": break_form.cleaned_data["start_time"].strftime("%H:%M:%S"), - # "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"), - # } - # ) - - # Create a temporary schedule object (not saved to DB) - temp_schedule = InterviewSchedule( - job=job, - start_date=start_date, - end_date=end_date, - working_days=working_days, - start_time=start_time, - end_time=end_time, - interview_duration=interview_duration, - buffer_time=buffer_time or 5, - break_start_time=break_start_time or None, - break_end_time=break_end_time or None, - ) - - # Get available slots (temp_breaks logic moved into get_available_time_slots if needed) - available_slots = get_available_time_slots(temp_schedule) - - if len(available_slots) < len(applications): - messages.error( - request, - f"Not enough available slots. Required: {len(applications)}, Available: {len(available_slots)}", - ) - return render( - request, - "interviews/schedule_interviews.html", - {"form": form, "job": job}, - ) - - # Create a preview schedule - preview_schedule = [] - for i, application in enumerate(applications): - slot = available_slots[i] - preview_schedule.append( - {"application": application, "date": slot["date"], "time": slot["time"]} - ) - - # Save the form data to session for later use - schedule_data = { - "start_date": start_date.isoformat(), - "end_date": end_date.isoformat(), - "working_days": working_days, - "start_time": start_time.isoformat(), - "end_time": end_time.isoformat(), - "interview_duration": interview_duration, - "buffer_time": buffer_time, - "break_start_time": break_start_time.isoformat() if break_start_time else None, - "break_end_time": break_end_time.isoformat() if break_end_time else None, - "candidate_ids": [c.id for c in applications], - "schedule_interview_type":schedule_interview_type - - } - request.session[SESSION_DATA_KEY] = schedule_data - - # Render the preview page - return render( - request, - "interviews/preview_schedule.html", - { - "job": job, - "schedule": preview_schedule, - "start_date": start_date, - "end_date": end_date, - "working_days": working_days, - "start_time": start_time, - "end_time": end_time, - "break_start_time": break_start_time, - "break_end_time": break_end_time, - "interview_duration": interview_duration, - "buffer_time": buffer_time, - "schedule_interview_type":schedule_interview_type, - "form":OnsiteLocationForm() - }, - ) - else: - # Re-render the form if validation fails - return render( - request, - "interviews/schedule_interviews.html", - {"form": form, "job": job}, - ) +# def schedule_interviews_view(request, slug): +# job = get_object_or_404(JobPosting, slug=slug) +# if request.method == "POST": +# # return _handle_confirm_schedule(request, slug, job) +# return _handle_preview_submission(request, slug, job) +# else: +# return _handle_get_request(request, slug, job) - -def _handle_confirm_schedule(request, slug, job): - """ - Handles the final POST request (Confirm Schedule). - Creates the main schedule record and queues individual interviews asynchronously. - """ - - SESSION_DATA_KEY = "interview_schedule_data" - SESSION_ID_KEY = f"schedule_candidate_ids_{slug}" - - # 1. Get schedule data from session - schedule_data = request.session.get(SESSION_DATA_KEY) - - if not schedule_data: - messages.error(request, "Session expired. Please try again.") - return redirect("schedule_interviews", slug=slug) - - # 2. Create the Interview Schedule (Parent Record) - try: - # Handle break times: If they exist, convert them; otherwise, pass None. - break_start = schedule_data.get("break_start_time") - break_end = schedule_data.get("break_end_time") - - schedule = InterviewSchedule.objects.create( - job=job, - created_by=request.user, - start_date=datetime.fromisoformat(schedule_data["start_date"]).date(), - end_date=datetime.fromisoformat(schedule_data["end_date"]).date(), - working_days=schedule_data["working_days"], - start_time=time.fromisoformat(schedule_data["start_time"]), - end_time=time.fromisoformat(schedule_data["end_time"]), - interview_duration=schedule_data["interview_duration"], - buffer_time=schedule_data["buffer_time"], - # Convert time strings to time objects only if they exist and handle None gracefully - break_start_time=time.fromisoformat(break_start) if break_start else None, - break_end_time=time.fromisoformat(break_end) if break_end else None, - schedule_interview_type=schedule_data.get("schedule_interview_type") - ) - except Exception as e: - # Clear data on failure to prevent stale data causing repeated errors - messages.error(request, f"Error creating schedule: {e}") - if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY] - if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY] - return redirect("schedule_interviews", slug=slug) - - # 3. Setup candidates and get slots - candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"]) - schedule.applications.set(candidates) - available_slots = get_available_time_slots(schedule) - - # 4. Handle Remote/Onsite logic - if schedule_data.get("schedule_interview_type") == 'Remote': - # ... (Remote logic remains unchanged) - queued_count = 0 - for i, candidate in enumerate(candidates): - if i < len(available_slots): - slot = available_slots[i] - - async_task( - "recruitment.tasks.create_interview_and_meeting", - candidate.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration, - ) - queued_count += 1 - - messages.success( - request, - f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!", - ) - - 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("job_detail", slug=slug) - - elif schedule_data.get("schedule_interview_type") == 'Onsite': - print("inside...") - - if request.method == 'POST': - form = OnsiteLocationForm(request.POST) - - if form.is_valid(): - - if not available_slots: - messages.error(request, "No available slots found for the selected schedule range.") - return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) - - # Extract common location data from the form - physical_address = form.cleaned_data['physical_address'] - room_number = form.cleaned_data['room_number'] - topic=form.cleaned_data['topic'] - - - try: - # 1. Iterate over candidates and create a NEW Location object for EACH - for i, candidate in enumerate(candidates): - if i < len(available_slots): - slot = available_slots[i] - - - location_start_dt = datetime.combine(slot['date'], schedule.start_time) - - # --- CORE FIX: Create a NEW Location object inside the loop --- - onsite_location = OnsiteLocationDetails.objects.create( - start_time=location_start_dt, - duration=schedule.interview_duration, - physical_address=physical_address, - room_number=room_number, - location_type="Onsite", - topic=topic - - ) - - # 2. Create the ScheduledInterview, linking the unique location - ScheduledInterview.objects.create( - application=candidate, - job=job, - schedule=schedule, - interview_date=slot['date'], - interview_time=slot['time'], - interview_location=onsite_location, - ) - - messages.success( - request, - f"Onsite schedule interviews created successfully for {len(candidates)} candidates." - ) - - # 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('job_detail', slug=job.slug) - - except Exception as e: - messages.error(request, f"Error creating onsite location/interviews: {e}") - # On failure, re-render the form with the error and ensure 'job' is present - return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) - - else: - # Form is invalid, re-render with errors - # Ensure 'job' is passed to prevent NoReverseMatch - return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) - - else: - # For a GET request - form = OnsiteLocationForm() - - return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) - - - -def schedule_interviews_view(request, slug): - job = get_object_or_404(JobPosting, slug=slug) - if request.method == "POST": - # return _handle_confirm_schedule(request, slug, job) - return _handle_preview_submission(request, slug, job) - else: - return _handle_get_request(request, slug, job) - - -def confirm_schedule_interviews_view(request, slug): - job = get_object_or_404(JobPosting, slug=slug) - if request.method == "POST": - return _handle_confirm_schedule(request, slug, job) +# def confirm_schedule_interviews_view(request, slug): +# job = get_object_or_404(JobPosting, slug=slug) +# if request.method == "POST": +# return _handle_confirm_schedule(request, slug, job) @staff_user_required @@ -1912,7 +1906,7 @@ def application_update_status(request, slug): mark_as = request.POST.get("mark_as") if mark_as != "----------": application_ids = request.POST.getlist("candidate_ids") - + if c := Application.objects.filter(pk__in=application_ids): if mark_as == "Exam": print("exam") @@ -2006,7 +2000,6 @@ def applications_document_review_view(request, slug): # Get candidates from Interview stage who need document review applications = job.document_review_applications.select_related('person') - # Get search query for filtering search_query = request.GET.get('q', '') if search_query: @@ -2025,127 +2018,128 @@ def applications_document_review_view(request, slug): return render(request, "recruitment/applications_document_review_view.html", context) -@staff_user_required -def reschedule_meeting_for_application(request, slug, candidate_id, meeting_id): - job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_id) - meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) - form = ZoomMeetingForm(instance=meeting) +# @staff_user_required +# def reschedule_meeting_for_application(request, slug, candidate_id, meeting_id): +# job = get_object_or_404(JobPosting, slug=slug) +# candidate = get_object_or_404(Application, pk=candidate_id) +# meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) +# form = ZoomMeetingForm(instance=meeting) - if request.method == "POST": - form = ZoomMeetingForm(request.POST, instance=meeting) - if form.is_valid(): - instance = form.save(commit=False) - updated_data = { - "topic": instance.topic, - "start_time": instance.start_time.isoformat() + "Z", - "duration": instance.duration, - } - if instance.start_time < timezone.now(): - messages.error(request, "Start time must be in the future.") - return redirect( - "reschedule_meeting_for_application", - slug=job.slug, - candidate_id=candidate_id, - meeting_id=meeting_id, - ) +# if request.method == "POST": +# form = ZoomMeetingForm(request.POST, instance=meeting) +# if form.is_valid(): +# instance = form.save(commit=False) +# updated_data = { +# "topic": instance.topic, +# "start_time": instance.start_time.isoformat() + "Z", +# "duration": instance.duration, +# } +# if instance.start_time < timezone.now(): +# messages.error(request, "Start time must be in the future.") +# return redirect( +# "reschedule_meeting_for_application", +# slug=job.slug, +# candidate_id=candidate_id, +# meeting_id=meeting_id, +# ) - result = update_meeting(instance, updated_data) +# result = update_meeting(instance, updated_data) - if result["status"] == "success": - messages.success(request, result["message"]) - else: - messages.error(request, result["message"]) - return redirect( - reverse("applications_interview_view", kwargs={"slug": job.slug}) - ) + # if result["status"] == "success": + # messages.success(request, result["message"]) + # else: + # messages.error(request, result["message"]) + # return redirect( + # reverse("applications_interview_view", kwargs={"slug": job.slug}) + # ) - context = {"job": job, "candidate": candidate, "meeting": meeting, "form": form} - return render(request, "meetings/reschedule_meeting.html", context) +# context = {"job": job, "candidate": candidate, "meeting": meeting, "form": form} +# return render(request, "meetings/reschedule_meeting.html", context) -@staff_user_required -def schedule_meeting_for_application(request, slug, candidate_pk, meeting_id): - job = get_object_or_404(JobPosting, slug=slug) - application = get_object_or_404(Application, pk=candidate_pk) - meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) - if request.method == "POST": - result = delete_zoom_meeting(meeting.meeting_id) - if ( - result["status"] == "success" - or "Meeting does not exist" in result["details"]["message"] - ): - meeting.delete() - messages.success(request, "Meeting deleted successfully") - else: - messages.error(request, result["message"]) - return redirect(reverse("applications_interview_view", kwargs={"slug": job.slug})) +# @staff_user_required +# def schedule_meeting_for_application(request, slug, candidate_pk, meeting_id): +# job = get_object_or_404(JobPosting, slug=slug) +# application = get_object_or_404(Application, pk=candidate_pk) +# meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) +# if request.method == "POST": +# result = delete_zoom_meeting(meeting.meeting_id) +# if ( +# result["status"] == "success" +# or "Meeting does not exist" in result["details"]["message"] +# ): +# meeting.delete() +# messages.success(request, "Meeting deleted successfully") +# else: +# messages.error(request, result["message"]) +# return redirect(reverse("applications_interview_view", kwargs={"slug": job.slug})) - context = { - "job": job, - "application": application, - "meeting": meeting, - "delete_url": reverse( - "schedule_meeting_for_application", - kwargs={ - "slug": job.slug, - "candidate_pk": candidate_pk, - "meeting_id": meeting_id, - }, - ), - } - return render(request, "meetings/delete_meeting_form.html", context) + # context = { + # "job": job, + # "application": application, + # "meeting": meeting, + # "delete_url": reverse( + # "schedule_meeting_for_application", + # kwargs={ + # "slug": job.slug, + # "candidate_pk": candidate_pk, + # "meeting_id": meeting_id, + # }, + # ), + # } + # return render(request, "meetings/delete_meeting_form.html", context) -@staff_user_required -def delete_zoom_meeting_for_candidate(request, slug, candidate_pk, meeting_id): - """ - Deletes a specific Zoom (Remote) meeting instance. - The ZoomMeetingDetails object inherits from InterviewLocation, - which is linked to ScheduledInterview. Deleting the subclass - should trigger CASCADE/SET_NULL correctly on the FK chain. - """ - job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_pk) +# @staff_user_required +# def delete_zoom_meeting_for_candidate(request, slug, candidate_pk, meeting_id): +# """ +# Deletes a specific Zoom (Remote) meeting instance. +# The ZoomMeetingDetails object inherits from InterviewLocation, +# which is linked to ScheduledInterview. Deleting the subclass +# should trigger CASCADE/SET_NULL correctly on the FK chain. +# """ +# job = get_object_or_404(JobPosting, slug=slug) +# candidate = get_object_or_404(Application, pk=candidate_pk) - # Target the specific Zoom meeting details instance - meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) +# # Target the specific Zoom meeting details instance +# # meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) +# meeting = None#TODO:Update - if request.method == "POST": - # 1. Attempt to delete the meeting from the external Zoom API - result = delete_zoom_meeting(meeting.meeting_id) +# if request.method == "POST": +# # 1. Attempt to delete the meeting from the external Zoom API +# result = delete_zoom_meeting(meeting.meeting_id) - # 2. Check for success OR if the meeting was already deleted externally - if ( - result["status"] == "success" - or "Meeting does not exist" in result["details"]["message"] - ): - # 3. Delete the local Django object. This will delete the base - # InterviewLocation object and update the ScheduledInterview FK. - meeting.delete() - messages.success(request, f"Remote meeting for {candidate.name} deleted successfully.") - else: - messages.error(request, result["message"]) +# # 2. Check for success OR if the meeting was already deleted externally +# if ( +# result["status"] == "success" +# or "Meeting does not exist" in result["details"]["message"] +# ): +# # 3. Delete the local Django object. This will delete the base +# # InterviewLocation object and update the ScheduledInterview FK. +# meeting.delete() +# messages.success(request, f"Remote meeting for {candidate.name} deleted successfully.") +# else: +# messages.error(request, result["message"]) - return redirect(reverse("applications_interview_view", kwargs={"slug": job.slug})) + # return redirect(reverse("applications_interview_view", kwargs={"slug": job.slug})) - context = { - "job": job, - "candidate": candidate, - "meeting": meeting, - "location_type": "Remote", - "delete_url": reverse( - "delete_remote_meeting_for_candidate", # Use the specific new URL name - kwargs={ - "slug": job.slug, - "candidate_pk": candidate_pk, - "meeting_id": meeting_id, - }, - ), - } - return render(request, "meetings/delete_meeting_form.html", context) +# context = { +# "job": job, +# "candidate": candidate, +# "meeting": meeting, +# "location_type": "Remote", +# "delete_url": reverse( +# "delete_remote_meeting_for_candidate", # Use the specific new URL name +# kwargs={ +# "slug": job.slug, +# "candidate_pk": candidate_pk, +# "meeting_id": meeting_id, +# }, +# ), +# } +# return render(request, "meetings/delete_meeting_form.html", context) -@staff_user_required -def interview_calendar_view(request, slug): +# @staff_user_required +# def interview_calendar_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) # Get all scheduled interviews for this job @@ -2204,331 +2198,332 @@ def interview_calendar_view(request, slug): return render(request, "recruitment/interview_calendar.html", context) -@staff_user_required -def interview_detail_view(request, slug, interview_id): - job = get_object_or_404(JobPosting, slug=slug) - interview = get_object_or_404(ScheduledInterview, id=interview_id, job=job) +# @staff_user_required +# def interview_detail_view(request, slug, interview_id): +# job = get_object_or_404(JobPosting, slug=slug) +# interview = get_object_or_404(ScheduledInterview, id=interview_id, job=job) - context = { - "job": job, - "interview": interview, - } +# context = { +# "job": job, +# "interview": interview, +# } - return render(request, "recruitment/interview_detail.html", context) +# return render(request, "recruitment/interview_detail.html", context) # Candidate Meeting Scheduling/Rescheduling Views -@require_POST -def api_schedule_application_meeting(request, job_slug, candidate_pk): - """ - Handle POST request to schedule a Zoom meeting for a candidate via HTMX. - Returns JSON response for modal update. - """ - job = get_object_or_404(JobPosting, slug=job_slug) - candidate = get_object_or_404(Application, pk=candidate_pk, job=job) +# @require_POST +# def api_schedule_application_meeting(request, job_slug, candidate_pk): +# """ +# Handle POST request to schedule a Zoom meeting for a candidate via HTMX. +# Returns JSON response for modal update. +# """ +# job = get_object_or_404(JobPosting, slug=job_slug) +# candidate = get_object_or_404(Application, pk=candidate_pk, job=job) - topic = f"Interview: {job.title} with {candidate.name}" - start_time_str = request.POST.get("start_time") - duration = int(request.POST.get("duration", 60)) +# topic = f"Interview: {job.title} with {candidate.name}" +# start_time_str = request.POST.get("start_time") +# duration = int(request.POST.get("duration", 60)) - if not start_time_str: - return JsonResponse( - {"success": False, "error": "Start time is required."}, status=400 - ) +# if not start_time_str: +# return JsonResponse( +# {"success": False, "error": "Start time is required."}, status=400 +# ) - try: - # Parse datetime from datetime-local input (YYYY-MM-DDTHH:MM) - # This will be in server's timezone, create_zoom_meeting will handle UTC conversion - naive_start_time = datetime.fromisoformat(start_time_str) - # Ensure it's timezone-aware if your system requires it, or let create_zoom_meeting handle it. - # For simplicity, assuming create_zoom_meeting handles naive datetimes or they are in UTC. - # If start_time is expected to be in a specific timezone, convert it here. - # e.g., start_time = timezone.make_aware(naive_start_time, timezone.get_current_timezone()) - start_time = naive_start_time # Or timezone.make_aware(naive_start_time) - except ValueError: - return JsonResponse( - {"success": False, "error": "Invalid date/time format for start time."}, - status=400, - ) +# try: +# # Parse datetime from datetime-local input (YYYY-MM-DDTHH:MM) +# # This will be in server's timezone, create_zoom_meeting will handle UTC conversion +# naive_start_time = datetime.fromisoformat(start_time_str) +# # Ensure it's timezone-aware if your system requires it, or let create_zoom_meeting handle it. +# # For simplicity, assuming create_zoom_meeting handles naive datetimes or they are in UTC. +# # If start_time is expected to be in a specific timezone, convert it here. +# # e.g., start_time = timezone.make_aware(naive_start_time, timezone.get_current_timezone()) +# start_time = naive_start_time # Or timezone.make_aware(naive_start_time) +# except ValueError: +# return JsonResponse( +# {"success": False, "error": "Invalid date/time format for start time."}, +# status=400, +# ) - if start_time <= timezone.now(): - return JsonResponse( - {"success": False, "error": "Start time must be in the future."}, status=400 - ) +# if start_time <= timezone.now(): +# return JsonResponse( +# {"success": False, "error": "Start time must be in the future."}, status=400 +# ) - result = create_zoom_meeting(topic=topic, start_time=start_time, duration=duration) +# result = create_zoom_meeting(topic=topic, start_time=start_time, duration=duration) - if result["status"] == "success": - zoom_meeting_details = result["meeting_details"] - zoom_meeting = ZoomMeetingDetails.objects.create( - topic=topic, - start_time=start_time, # Store in local timezone - duration=duration, - meeting_id=zoom_meeting_details["meeting_id"], - join_url=zoom_meeting_details["join_url"], - password=zoom_meeting_details["password"], - # host_email=zoom_meeting_details["host_email"], - status=result["zoom_gateway_response"].get("status", "waiting"), - zoom_gateway_response=result["zoom_gateway_response"], - ) - scheduled_interview = ScheduledInterview.objects.create( - candidate=candidate, - job=job, - zoom_meeting=zoom_meeting, - interview_date=start_time.date(), - interview_time=start_time.time(), - status="scheduled", # Or 'confirmed' depending on your workflow - ) - messages.success(request, f"Meeting scheduled with {candidate.name}.") +# if result["status"] == "success": +# zoom_meeting_details = result["meeting_details"] +# #TODO:update +# # zoom_meeting = ZoomMeetingDetails.objects.create( +# # topic=topic, +# # start_time=start_time, # Store in local timezone +# # duration=duration, +# # meeting_id=zoom_meeting_details["meeting_id"], +# # join_url=zoom_meeting_details["join_url"], +# # password=zoom_meeting_details["password"], +# # # host_email=zoom_meeting_details["host_email"], +# # status=result["zoom_gateway_response"].get("status", "waiting"), +# # zoom_gateway_response=result["zoom_gateway_response"], +# # ) +# scheduled_interview = ScheduledInterview.objects.create( +# candidate=candidate, +# job=job, +# # zoom_meeting=zoom_meeting,#TODO:update +# interview_date=start_time.date(), +# interview_time=start_time.time(), +# status="scheduled", # Or 'confirmed' depending on your workflow +# ) +# messages.success(request, f"Meeting scheduled with {candidate.name}.") - # Return updated table row or a success message - # For HTMX, you might want to return a fragment of the updated table - # For now, returning JSON to indicate success and close modal - return JsonResponse( - { - "success": True, - "message": "Meeting scheduled successfully!", - "join_url": zoom_meeting.join_url, - "meeting_id": zoom_meeting.meeting_id, - "candidate_name": candidate.name, - "interview_datetime": start_time.strftime("%Y-%m-%d %H:%M"), - } - ) - else: - messages.error(request, result["message"]) - return JsonResponse({"success": False, "error": result["message"]}, status=400) +# # Return updated table row or a success message +# # For HTMX, you might want to return a fragment of the updated table +# # For now, returning JSON to indicate success and close modal +# return JsonResponse( +# { +# "success": True, +# "message": "Meeting scheduled successfully!", +# "join_url": zoom_meeting.join_url, +# "meeting_id": zoom_meeting.meeting_id, +# "candidate_name": candidate.name, +# "interview_datetime": start_time.strftime("%Y-%m-%d %H:%M"), +# } +# ) +# else: +# messages.error(request, result["message"]) +# return JsonResponse({"success": False, "error": result["message"]}, status=400) -def schedule_application_meeting(request, job_slug, candidate_pk): - """ - GET: Render modal form to schedule a meeting. (For HTMX) - POST: Handled by api_schedule_application_meeting. - """ - job = get_object_or_404(JobPosting, slug=job_slug) - candidate = get_object_or_404(Application, pk=candidate_pk, job=job) +# def schedule_application_meeting(request, job_slug, candidate_pk): +# """ +# GET: Render modal form to schedule a meeting. (For HTMX) +# POST: Handled by api_schedule_application_meeting. +# """ +# job = get_object_or_404(JobPosting, slug=job_slug) +# candidate = get_object_or_404(Application, pk=candidate_pk, job=job) - if request.method == "POST": - return api_schedule_application_meeting(request, job_slug, candidate_pk) +# if request.method == "POST": +# return api_schedule_application_meeting(request, job_slug, candidate_pk) - # GET request - render the form snippet for HTMX - context = { - "job": job, - "candidate": candidate, - "action_url": reverse( - "api_schedule_application_meeting", - kwargs={"job_slug": job_slug, "candidate_pk": candidate_pk}, - ), - "scheduled_interview": None, # Explicitly None for schedule - } - # Render just the form part, or the whole modal body content - return render(request, "includes/meeting_form.html", context) +# # GET request - render the form snippet for HTMX +# context = { +# "job": job, +# "candidate": candidate, +# "action_url": reverse( +# "api_schedule_application_meeting", +# kwargs={"job_slug": job_slug, "candidate_pk": candidate_pk}, +# ), +# "scheduled_interview": None, # Explicitly None for schedule +# } +# # Render just the form part, or the whole modal body content +# return render(request, "includes/meeting_form.html", context) -@require_http_methods(["GET", "POST"]) -def api_schedule_application_meeting(request, job_slug, candidate_pk): - """ - Handles GET to render form and POST to process scheduling. - """ - job = get_object_or_404(JobPosting, slug=job_slug) - candidate = get_object_or_404(Application, pk=candidate_pk, job=job) +# @require_http_methods(["GET", "POST"]) +# def api_schedule_application_meeting(request, job_slug, candidate_pk): +# """ +# Handles GET to render form and POST to process scheduling. +# """ +# job = get_object_or_404(JobPosting, slug=job_slug) +# candidate = get_object_or_404(Application, pk=candidate_pk, job=job) - if request.method == "GET": - # This GET is for HTMX to fetch the form - context = { - "job": job, - "candidate": candidate, - "action_url": reverse( - "api_schedule_application_meeting", - kwargs={"job_slug": job_slug, "candidate_pk": candidate_pk}, - ), - "scheduled_interview": None, - } - return render(request, "includes/meeting_form.html", context) + # if request.method == "GET": + # # This GET is for HTMX to fetch the form + # context = { + # "job": job, + # "candidate": candidate, + # "action_url": reverse( + # "api_schedule_application_meeting", + # kwargs={"job_slug": job_slug, "candidate_pk": candidate_pk}, + # ), + # "scheduled_interview": None, + # } + # return render(request, "includes/meeting_form.html", context) - # POST logic (remains the same) - topic = f"Interview: {job.title} with {candidate.name}" - start_time_str = request.POST.get("start_time") - duration = int(request.POST.get("duration", 60)) +# # POST logic (remains the same) +# topic = f"Interview: {job.title} with {candidate.name}" +# start_time_str = request.POST.get("start_time") +# duration = int(request.POST.get("duration", 60)) - if not start_time_str: - return JsonResponse( - {"success": False, "error": "Start time is required."}, status=400 - ) +# if not start_time_str: +# return JsonResponse( +# {"success": False, "error": "Start time is required."}, status=400 +# ) - try: - naive_start_time = datetime.fromisoformat(start_time_str) - start_time = naive_start_time - except ValueError: - return JsonResponse( - {"success": False, "error": "Invalid date/time format for start time."}, - status=400, - ) +# try: +# naive_start_time = datetime.fromisoformat(start_time_str) +# start_time = naive_start_time +# except ValueError: +# return JsonResponse( +# {"success": False, "error": "Invalid date/time format for start time."}, +# status=400, +# ) - if start_time <= timezone.now(): - return JsonResponse( - {"success": False, "error": "Start time must be in the future."}, status=400 - ) +# if start_time <= timezone.now(): +# return JsonResponse( +# {"success": False, "error": "Start time must be in the future."}, status=400 +# ) - result = create_zoom_meeting(topic=topic, start_time=start_time, duration=duration) +# result = create_zoom_meeting(topic=topic, start_time=start_time, duration=duration) - if result["status"] == "success": - zoom_meeting_details = result["meeting_details"] - zoom_meeting = ZoomMeetingDetails.objects.create( - topic=topic, - start_time=start_time, - duration=duration, - meeting_id=zoom_meeting_details["meeting_id"], - join_url=zoom_meeting_details["join_url"], - password=zoom_meeting_details["password"], - host_email=zoom_meeting_details["host_email"], - status=result["zoom_gateway_response"].get("status", "waiting"), - zoom_gateway_response=result["zoom_gateway_response"], - ) - scheduled_interview = ScheduledInterview.objects.create( - candidate=candidate, - job=job, - zoom_meeting=zoom_meeting, - interview_date=start_time.date(), - interview_time=start_time.time(), - status="scheduled", - ) - messages.success(request, f"Meeting scheduled with {candidate.name}.") - return JsonResponse( - { - "success": True, - "message": "Meeting scheduled successfully!", - "join_url": zoom_meeting.join_url, - "meeting_id": zoom_meeting.meeting_id, - "candidate_name": candidate.name, - "interview_datetime": start_time.strftime("%Y-%m-%d %H:%M"), - } - ) - else: - messages.error(request, result["message"]) - return JsonResponse({"success": False, "error": result["message"]}, status=400) +# if result["status"] == "success": +# zoom_meeting_details = result["meeting_details"] +# # zoom_meeting = ZoomMeetingDetails.objects.create( +# # topic=topic, +# # start_time=start_time, +# # duration=duration, +# # meeting_id=zoom_meeting_details["meeting_id"], +# # join_url=zoom_meeting_details["join_url"], +# # password=zoom_meeting_details["password"], +# # host_email=zoom_meeting_details["host_email"], +# # status=result["zoom_gateway_response"].get("status", "waiting"), +# # zoom_gateway_response=result["zoom_gateway_response"], +# # ) +# scheduled_interview = ScheduledInterview.objects.create( +# candidate=candidate, +# job=job, +# # zoom_meeting=zoom_meeting,TODO:Update +# interview_date=start_time.date(), +# interview_time=start_time.time(), +# status="scheduled", +# ) +# messages.success(request, f"Meeting scheduled with {candidate.name}.") +# return JsonResponse( +# { +# "success": True, +# "message": "Meeting scheduled successfully!", +# "join_url": zoom_meeting.join_url, +# "meeting_id": zoom_meeting.meeting_id, +# "candidate_name": candidate.name, +# "interview_datetime": start_time.strftime("%Y-%m-%d %H:%M"), +# } +# ) +# else: +# messages.error(request, result["message"]) +# return JsonResponse({"success": False, "error": result["message"]}, status=400) -@require_http_methods(["GET", "POST"]) -def api_reschedule_application_meeting(request, job_slug, candidate_pk, interview_pk): - """ - Handles GET to render form and POST to process rescheduling. - """ - job = get_object_or_404(JobPosting, slug=job_slug) - scheduled_interview = get_object_or_404( - ScheduledInterview.objects.select_related("zoom_meeting"), - pk=interview_pk, - application__pk=candidate_pk, - job=job, - ) - zoom_meeting = scheduled_interview.zoom_meeting +# @require_http_methods(["GET", "POST"]) +# def api_reschedule_application_meeting(request, job_slug, candidate_pk, interview_pk): +# """ +# Handles GET to render form and POST to process rescheduling. +# """ +# job = get_object_or_404(JobPosting, slug=job_slug) +# scheduled_interview = get_object_or_404( +# ScheduledInterview.objects.select_related("zoom_meeting"), +# pk=interview_pk, +# application__pk=candidate_pk, +# job=job, +# ) +# zoom_meeting = scheduled_interview.zoom_meeting - if request.method == "GET": - # This GET is for HTMX to fetch the form - initial_data = { - "topic": zoom_meeting.topic, - "start_time": zoom_meeting.start_time.strftime("%Y-%m-%dT%H:%M"), - "duration": zoom_meeting.duration, - } - context = { - "job": job, - "candidate": scheduled_interview.application, - "scheduled_interview": scheduled_interview, # Pass for conditional logic in template - "initial_data": initial_data, - "action_url": reverse( - "api_reschedule_application_meeting", - kwargs={ - "job_slug": job_slug, - "candidate_pk": candidate_pk, - "interview_pk": interview_pk, - }, - ), - } - return render(request, "includes/meeting_form.html", context) + # if request.method == "GET": + # # This GET is for HTMX to fetch the form + # initial_data = { + # "topic": zoom_meeting.topic, + # "start_time": zoom_meeting.start_time.strftime("%Y-%m-%dT%H:%M"), + # "duration": zoom_meeting.duration, + # } + # context = { + # "job": job, + # "candidate": scheduled_interview.application, + # "scheduled_interview": scheduled_interview, # Pass for conditional logic in template + # "initial_data": initial_data, + # "action_url": reverse( + # "api_reschedule_application_meeting", + # kwargs={ + # "job_slug": job_slug, + # "candidate_pk": candidate_pk, + # "interview_pk": interview_pk, + # }, + # ), + # } + # return render(request, "includes/meeting_form.html", context) - # POST logic (remains the same) - new_start_time_str = request.POST.get("start_time") - new_duration = int(request.POST.get("duration", zoom_meeting.duration)) +# # POST logic (remains the same) +# new_start_time_str = request.POST.get("start_time") +# new_duration = int(request.POST.get("duration", zoom_meeting.duration)) - if not new_start_time_str: - return JsonResponse( - {"success": False, "error": "New start time is required."}, status=400 - ) +# if not new_start_time_str: +# return JsonResponse( +# {"success": False, "error": "New start time is required."}, status=400 +# ) - try: - naive_new_start_time = datetime.fromisoformat(new_start_time_str) - new_start_time = naive_new_start_time - except ValueError: - return JsonResponse( - {"success": False, "error": "Invalid date/time format for new start time."}, - status=400, - ) +# try: +# naive_new_start_time = datetime.fromisoformat(new_start_time_str) +# new_start_time = naive_new_start_time +# except ValueError: +# return JsonResponse( +# {"success": False, "error": "Invalid date/time format for new start time."}, +# status=400, +# ) - if new_start_time <= timezone.now(): - return JsonResponse( - {"success": False, "error": "Start time must be in the future."}, status=400 - ) +# if new_start_time <= timezone.now(): +# return JsonResponse( +# {"success": False, "error": "Start time must be in the future."}, status=400 +# ) - updated_data = { - "topic": f"Interview: {job.title} with {scheduled_interview.candidate.name}", - "start_time": new_start_time.isoformat() + "Z", - "duration": new_duration, - } +# updated_data = { +# "topic": f"Interview: {job.title} with {scheduled_interview.candidate.name}", +# "start_time": new_start_time.isoformat() + "Z", +# "duration": new_duration, +# } - result = update_zoom_meeting(zoom_meeting.meeting_id, updated_data) +# result = update_zoom_meeting(zoom_meeting.meeting_id, updated_data) - if result["status"] == "success": - details_result = get_zoom_meeting_details(zoom_meeting.meeting_id) - if details_result["status"] == "success": - updated_zoom_details = details_result["meeting_details"] - zoom_meeting.topic = updated_zoom_details.get("topic", zoom_meeting.topic) - zoom_meeting.start_time = new_start_time - zoom_meeting.duration = new_duration - zoom_meeting.join_url = updated_zoom_details.get( - "join_url", zoom_meeting.join_url - ) - zoom_meeting.password = updated_zoom_details.get( - "password", zoom_meeting.password - ) - zoom_meeting.status = updated_zoom_details.get( - "status", zoom_meeting.status - ) - zoom_meeting.zoom_gateway_response = updated_zoom_details - zoom_meeting.save() +# if result["status"] == "success": +# details_result = get_zoom_meeting_details(zoom_meeting.meeting_id) +# if details_result["status"] == "success": +# updated_zoom_details = details_result["meeting_details"] +# zoom_meeting.topic = updated_zoom_details.get("topic", zoom_meeting.topic) +# zoom_meeting.start_time = new_start_time +# zoom_meeting.duration = new_duration +# zoom_meeting.join_url = updated_zoom_details.get( +# "join_url", zoom_meeting.join_url +# ) +# zoom_meeting.password = updated_zoom_details.get( +# "password", zoom_meeting.password +# ) +# zoom_meeting.status = updated_zoom_details.get( +# "status", zoom_meeting.status +# ) +# zoom_meeting.zoom_gateway_response = updated_zoom_details +# zoom_meeting.save() - scheduled_interview.interview_date = new_start_time.date() - scheduled_interview.interview_time = new_start_time.time() - scheduled_interview.status = "rescheduled" - scheduled_interview.save() - messages.success( - request, - f"Meeting for {scheduled_interview.candidate.name} rescheduled.", - ) - else: - logger.warning( - f"Zoom meeting {zoom_meeting.meeting_id} updated, but failed to fetch latest details." - ) - zoom_meeting.start_time = new_start_time - zoom_meeting.duration = new_duration - zoom_meeting.save() - scheduled_interview.interview_date = new_start_time.date() - scheduled_interview.interview_time = new_start_time.time() - scheduled_interview.save() - messages.success( - request, - f"Meeting for {scheduled_interview.candidate.name} rescheduled. (Note: Could not refresh all details from Zoom.)", - ) +# scheduled_interview.interview_date = new_start_time.date() +# scheduled_interview.interview_time = new_start_time.time() +# scheduled_interview.status = "rescheduled" +# scheduled_interview.save() +# messages.success( +# request, +# f"Meeting for {scheduled_interview.candidate.name} rescheduled.", +# ) +# else: +# logger.warning( +# f"Zoom meeting {zoom_meeting.meeting_id} updated, but failed to fetch latest details." +# ) +# zoom_meeting.start_time = new_start_time +# zoom_meeting.duration = new_duration +# zoom_meeting.save() +# scheduled_interview.interview_date = new_start_time.date() +# scheduled_interview.interview_time = new_start_time.time() +# scheduled_interview.save() +# messages.success( +# request, +# f"Meeting for {scheduled_interview.candidate.name} rescheduled. (Note: Could not refresh all details from Zoom.)", +# ) - return JsonResponse( - { - "success": True, - "message": "Meeting rescheduled successfully!", - "join_url": zoom_meeting.join_url, - "new_interview_datetime": new_start_time.strftime("%Y-%m-%d %H:%M"), - } - ) - else: - messages.error(request, result["message"]) - return JsonResponse({"success": False, "error": result["message"]}, status=400) +# return JsonResponse( +# { +# "success": True, +# "message": "Meeting rescheduled successfully!", +# "join_url": zoom_meeting.join_url, +# "new_interview_datetime": new_start_time.strftime("%Y-%m-%d %H:%M"), +# } +# ) +# else: +# messages.error(request, result["message"]) +# return JsonResponse({"success": False, "error": result["message"]}, status=400) # The original schedule_application_meeting and reschedule_application_meeting (without api_ prefix) @@ -2538,349 +2533,349 @@ def api_reschedule_application_meeting(request, job_slug, candidate_pk, intervie # For now, let's assume the api_ versions are the primary ones for HTMX. -def reschedule_application_meeting(request, job_slug, candidate_pk, interview_pk): - """ - Handles GET to display a form for rescheduling a meeting. - Handles POST to process the rescheduling of a meeting. - """ - job = get_object_or_404(JobPosting, slug=job_slug) - application = get_object_or_404(Application, pk=candidate_pk, job=job) - scheduled_interview = get_object_or_404( - ScheduledInterview.objects.select_related("zoom_meeting"), - pk=interview_pk, - application=application, - job=job, - ) - zoom_meeting = scheduled_interview.zoom_meeting +# def reschedule_application_meeting(request, job_slug, candidate_pk, interview_pk): +# """ +# Handles GET to display a form for rescheduling a meeting. +# Handles POST to process the rescheduling of a meeting. +# """ +# job = get_object_or_404(JobPosting, slug=job_slug) +# application = get_object_or_404(Application, pk=candidate_pk, job=job) +# scheduled_interview = get_object_or_404( +# ScheduledInterview.objects.select_related("zoom_meeting"), +# pk=interview_pk, +# application=application, +# job=job, +# ) +# zoom_meeting = scheduled_interview.zoom_meeting - # Determine if the candidate has other future meetings - # This helps in providing context in the template - # Note: This checks for *any* future meetings for the candidate, not just the one being rescheduled. - # If candidate.has_future_meeting is True, it implies they have at least one other upcoming meeting, - # or the specific meeting being rescheduled is itself in the future. - # We can refine this logic if needed, e.g., check for meetings *other than* the current `interview_pk`. - has_other_future_meetings = application.has_future_meeting - # More precise check: if the current meeting being rescheduled is in the future, then by definition - # the candidate will have a future meeting (this one). The UI might want to know if there are *others*. - # For now, `candidate.has_future_meeting` is a good general indicator. +# # Determine if the candidate has other future meetings +# # This helps in providing context in the template +# # Note: This checks for *any* future meetings for the candidate, not just the one being rescheduled. +# # If candidate.has_future_meeting is True, it implies they have at least one other upcoming meeting, +# # or the specific meeting being rescheduled is itself in the future. +# # We can refine this logic if needed, e.g., check for meetings *other than* the current `interview_pk`. +# has_other_future_meetings = application.has_future_meeting +# # More precise check: if the current meeting being rescheduled is in the future, then by definition +# # the candidate will have a future meeting (this one). The UI might want to know if there are *others*. +# # For now, `candidate.has_future_meeting` is a good general indicator. - if request.method == "POST": - form = ZoomMeetingForm(request.POST) - if form.is_valid(): - new_topic = form.cleaned_data.get("topic") - new_start_time = form.cleaned_data.get("start_time") - new_duration = form.cleaned_data.get("duration") +# if request.method == "POST": +# form = ZoomMeetingForm(request.POST) +# if form.is_valid(): +# new_topic = form.cleaned_data.get("topic") +# new_start_time = form.cleaned_data.get("start_time") +# new_duration = form.cleaned_data.get("duration") - # Use a default topic if not provided, keeping with the original structure - if not new_topic: - new_topic = f"Interview: {job.title} with {application.name}" +# # Use a default topic if not provided, keeping with the original structure +# if not new_topic: +# new_topic = f"Interview: {job.title} with {application.name}" # Ensure new_start_time is in the future - if new_start_time <= timezone.now(): - messages.error(request, "Start time must be in the future.") - # Re-render form with error and initial data - return render( - request, - "recruitment/schedule_meeting_form.html", - { # Reusing the same form template - "form": form, - "job": job, - "application": application, - "scheduled_interview": scheduled_interview, - "initial_topic": new_topic, - "initial_start_time": new_start_time.strftime("%Y-%m-%dT%H:%M") - if new_start_time - else "", - "initial_duration": new_duration, - "action_url": reverse( - "reschedule_application_meeting", - kwargs={ - "job_slug": job_slug, - "candidate_pk": candidate_pk, - "interview_pk": interview_pk, - }, - ), - "has_future_meeting": has_other_future_meetings, # Pass status for template - }, - ) + # if new_start_time <= timezone.now(): + # messages.error(request, "Start time must be in the future.") + # # Re-render form with error and initial data + # return render( + # request, + # "recruitment/schedule_meeting_form.html", + # { # Reusing the same form template + # "form": form, + # "job": job, + # "application": application, + # "scheduled_interview": scheduled_interview, + # "initial_topic": new_topic, + # "initial_start_time": new_start_time.strftime("%Y-%m-%dT%H:%M") + # if new_start_time + # else "", + # "initial_duration": new_duration, + # "action_url": reverse( + # "reschedule_application_meeting", + # kwargs={ + # "job_slug": job_slug, + # "candidate_pk": candidate_pk, + # "interview_pk": interview_pk, + # }, + # ), + # "has_future_meeting": has_other_future_meetings, # Pass status for template + # }, + # ) - # Prepare data for Zoom API update - # The update_zoom_meeting expects start_time as ISO string with 'Z' - zoom_update_data = { - "topic": new_topic, - "start_time": new_start_time.isoformat() + "Z", - "duration": new_duration, - } +# # Prepare data for Zoom API update +# # The update_zoom_meeting expects start_time as ISO string with 'Z' +# zoom_update_data = { +# "topic": new_topic, +# "start_time": new_start_time.isoformat() + "Z", +# "duration": new_duration, +# } - # Update Zoom meeting using utility function - zoom_update_result = update_zoom_meeting( - zoom_meeting.meeting_id, zoom_update_data - ) +# # Update Zoom meeting using utility function +# zoom_update_result = update_zoom_meeting( +# zoom_meeting.meeting_id, zoom_update_data +# ) - if zoom_update_result["status"] == "success": - # Fetch the latest details from Zoom after successful update - details_result = get_zoom_meeting_details(zoom_meeting.meeting_id) +# if zoom_update_result["status"] == "success": +# # Fetch the latest details from Zoom after successful update +# details_result = get_zoom_meeting_details(zoom_meeting.meeting_id) - if details_result["status"] == "success": - updated_zoom_details = details_result["meeting_details"] - # Update local ZoomMeeting record - zoom_meeting.topic = updated_zoom_details.get("topic", new_topic) - zoom_meeting.start_time = ( - new_start_time # Store the original datetime - ) - zoom_meeting.duration = new_duration - zoom_meeting.join_url = updated_zoom_details.get( - "join_url", zoom_meeting.join_url - ) - zoom_meeting.password = updated_zoom_details.get( - "password", zoom_meeting.password - ) - zoom_meeting.status = updated_zoom_details.get( - "status", zoom_meeting.status - ) - zoom_meeting.zoom_gateway_response = details_result.get( - "meeting_details" - ) - zoom_meeting.save() +# if details_result["status"] == "success": +# updated_zoom_details = details_result["meeting_details"] +# # Update local ZoomMeeting record +# zoom_meeting.topic = updated_zoom_details.get("topic", new_topic) +# zoom_meeting.start_time = ( +# new_start_time # Store the original datetime +# ) +# zoom_meeting.duration = new_duration +# zoom_meeting.join_url = updated_zoom_details.get( +# "join_url", zoom_meeting.join_url +# ) +# zoom_meeting.password = updated_zoom_details.get( +# "password", zoom_meeting.password +# ) +# zoom_meeting.status = updated_zoom_details.get( +# "status", zoom_meeting.status +# ) +# zoom_meeting.zoom_gateway_response = details_result.get( +# "meeting_details" +# ) +# zoom_meeting.save() - # Update ScheduledInterview record - scheduled_interview.interview_date = new_start_time.date() - scheduled_interview.interview_time = new_start_time.time() - scheduled_interview.status = ( - "rescheduled" # Or 'scheduled' if you prefer - ) - scheduled_interview.save() - messages.success( - request, - f"Meeting for {application.name} rescheduled successfully.", - ) - else: - # If fetching details fails, update with form data and log a warning - logger.warning( - f"Successfully updated Zoom meeting {zoom_meeting.meeting_id}, but failed to fetch updated details. " - f"Error: {details_result.get('message', 'Unknown error')}" - ) - # Update with form data as a fallback - zoom_meeting.topic = new_topic - zoom_meeting.start_time = new_start_time - zoom_meeting.duration = new_duration - zoom_meeting.save() - scheduled_interview.interview_date = new_start_time.date() - scheduled_interview.interview_time = new_start_time.time() - scheduled_interview.save() - messages.success( - request, - f"Meeting for {application.name} rescheduled. (Note: Could not refresh all details from Zoom.)", - ) +# # Update ScheduledInterview record +# scheduled_interview.interview_date = new_start_time.date() +# scheduled_interview.interview_time = new_start_time.time() +# scheduled_interview.status = ( +# "rescheduled" # Or 'scheduled' if you prefer +# ) +# scheduled_interview.save() +# messages.success( +# request, +# f"Meeting for {application.name} rescheduled successfully.", +# ) +# else: +# # If fetching details fails, update with form data and log a warning +# logger.warning( +# f"Successfully updated Zoom meeting {zoom_meeting.meeting_id}, but failed to fetch updated details. " +# f"Error: {details_result.get('message', 'Unknown error')}" +# ) +# # Update with form data as a fallback +# zoom_meeting.topic = new_topic +# zoom_meeting.start_time = new_start_time +# zoom_meeting.duration = new_duration +# zoom_meeting.save() +# scheduled_interview.interview_date = new_start_time.date() +# scheduled_interview.interview_time = new_start_time.time() +# scheduled_interview.save() +# messages.success( +# request, +# f"Meeting for {application.name} rescheduled. (Note: Could not refresh all details from Zoom.)", +# ) - return redirect("applications_interview_view", slug=job.slug) - else: - messages.error( - request, - f"Failed to update Zoom meeting: {zoom_update_result['message']}", - ) - # Re-render form with error - return render( - request, - "recruitment/schedule_meeting_form.html", - { - "form": form, - "job": job, - "application": application, - "scheduled_interview": scheduled_interview, - "initial_topic": new_topic, - "initial_start_time": new_start_time.strftime("%Y-%m-%dT%H:%M") - if new_start_time - else "", - "initial_duration": new_duration, - "action_url": reverse( - "reschedule_application_meeting", - kwargs={ - "job_slug": job_slug, - "candidate_pk": candidate_pk, - "interview_pk": interview_pk, - }, - ), - "has_future_meeting": has_other_future_meetings, - }, - ) - else: - # Form validation errors - return render( - request, - "recruitment/schedule_meeting_form.html", - { - "form": form, - "job": job, - "application": application, - "scheduled_interview": scheduled_interview, - "initial_topic": request.POST.get("topic", new_topic), - "initial_start_time": request.POST.get( - "start_time", - new_start_time.strftime("%Y-%m-%dT%H:%M") - if new_start_time - else "", - ), - "initial_duration": request.POST.get("duration", new_duration), - "action_url": reverse( - "reschedule_application_meeting", - kwargs={ - "job_slug": job_slug, - "candidate_pk": candidate_pk, - "interview_pk": interview_pk, - }, - ), - "has_future_meeting": has_other_future_meetings, - }, - ) - else: # GET request - # Pre-populate form with existing meeting details - initial_data = { - "topic": zoom_meeting.topic, - "start_time": zoom_meeting.start_time.strftime("%Y-%m-%dT%H:%M"), - "duration": zoom_meeting.duration, - } - form = ZoomMeetingForm(initial=initial_data) - return render( - request, - "recruitment/schedule_meeting_form.html", - { - "form": form, - "job": job, - "application": application, - "scheduled_interview": scheduled_interview, # Pass to template for title/differentiation - "action_url": reverse( - "reschedule_application_meeting", - kwargs={ - "job_slug": job_slug, - "candidate_pk": candidate_pk, - "interview_pk": interview_pk, - }, - ), - "has_future_meeting": has_other_future_meetings, # Pass status for template - }, - ) + # return redirect("applications_interview_view", slug=job.slug) + # else: + # messages.error( + # request, + # f"Failed to update Zoom meeting: {zoom_update_result['message']}", + # ) + # # Re-render form with error + # return render( + # request, + # "recruitment/schedule_meeting_form.html", + # { + # "form": form, + # "job": job, + # "application": application, + # "scheduled_interview": scheduled_interview, + # "initial_topic": new_topic, + # "initial_start_time": new_start_time.strftime("%Y-%m-%dT%H:%M") + # if new_start_time + # else "", + # "initial_duration": new_duration, + # "action_url": reverse( + # "reschedule_application_meeting", + # kwargs={ + # "job_slug": job_slug, + # "candidate_pk": candidate_pk, + # "interview_pk": interview_pk, + # }, + # ), + # "has_future_meeting": has_other_future_meetings, + # }, + # ) + # else: + # # Form validation errors + # return render( + # request, + # "recruitment/schedule_meeting_form.html", + # { + # "form": form, + # "job": job, + # "application": application, + # "scheduled_interview": scheduled_interview, + # "initial_topic": request.POST.get("topic", new_topic), + # "initial_start_time": request.POST.get( + # "start_time", + # new_start_time.strftime("%Y-%m-%dT%H:%M") + # if new_start_time + # else "", + # ), + # "initial_duration": request.POST.get("duration", new_duration), + # "action_url": reverse( + # "reschedule_application_meeting", + # kwargs={ + # "job_slug": job_slug, + # "candidate_pk": candidate_pk, + # "interview_pk": interview_pk, + # }, + # ), + # "has_future_meeting": has_other_future_meetings, + # }, + # ) + # else: # GET request + # # Pre-populate form with existing meeting details + # initial_data = { + # "topic": zoom_meeting.topic, + # "start_time": zoom_meeting.start_time.strftime("%Y-%m-%dT%H:%M"), + # "duration": zoom_meeting.duration, + # } + # form = ZoomMeetingForm(initial=initial_data) + # return render( + # request, + # "recruitment/schedule_meeting_form.html", + # { + # "form": form, + # "job": job, + # "application": application, + # "scheduled_interview": scheduled_interview, # Pass to template for title/differentiation + # "action_url": reverse( + # "reschedule_application_meeting", + # kwargs={ + # "job_slug": job_slug, + # "candidate_pk": candidate_pk, + # "interview_pk": interview_pk, + # }, + # ), + # "has_future_meeting": has_other_future_meetings, # Pass status for template + # }, + # ) -def schedule_meeting_for_application(request, slug, candidate_pk): - """ - Handles GET to display a simple form for scheduling a meeting for a candidate. - Handles POST to process the form, create a meeting, and redirect back. - """ - job = get_object_or_404(JobPosting, slug=slug) - application = get_object_or_404(Application, pk=candidate_pk, job=job) +# def schedule_meeting_for_application(request, slug, candidate_pk): +# """ +# Handles GET to display a simple form for scheduling a meeting for a candidate. +# Handles POST to process the form, create a meeting, and redirect back. +# """ +# job = get_object_or_404(JobPosting, slug=slug) +# application = get_object_or_404(Application, pk=candidate_pk, job=job) - if request.method == "POST": - form = ZoomMeetingForm(request.POST) - if form.is_valid(): - topic_val = form.cleaned_data.get("topic") - start_time_val = form.cleaned_data.get("start_time") - duration_val = form.cleaned_data.get("duration") +# # if request.method == "POST": +# # form = ZoomMeetingForm(request.POST) +# # if form.is_valid(): +# # topic_val = form.cleaned_data.get("topic") +# # start_time_val = form.cleaned_data.get("start_time") +# # duration_val = form.cleaned_data.get("duration") - # Use a default topic if not provided - if not topic_val: - topic_val = f"Interview: {job.title} with {application.name}" +# # Use a default topic if not provided +# if not topic_val: +# topic_val = f"Interview: {job.title} with {application.name}" - # Ensure start_time is in the future - if start_time_val <= timezone.now(): - messages.error(request, "Start time must be in the future.") - # Re-render form with error and initial data - return redirect("applications_interview_view", slug=job.slug) - # return render(request, "recruitment/schedule_meeting_form.html", { - # 'form': form, - # 'job': job, - # 'application': application, - # 'initial_topic': topic_val, - # 'initial_start_time': start_time_val.strftime('%Y-%m-%dT%H:%M') if start_time_val else '', - # 'initial_duration': duration_val - # }) +# # Ensure start_time is in the future +# if start_time_val <= timezone.now(): +# messages.error(request, "Start time must be in the future.") +# # Re-render form with error and initial data +# return redirect("applications_interview_view", slug=job.slug) +# # return render(request, "recruitment/schedule_meeting_form.html", { +# # 'form': form, +# # 'job': job, +# # 'application': application, +# # 'initial_topic': topic_val, +# # 'initial_start_time': start_time_val.strftime('%Y-%m-%dT%H:%M') if start_time_val else '', +# # 'initial_duration': duration_val +# # }) - # Create Zoom meeting using utility function - # The create_zoom_meeting expects start_time as a datetime object - # and handles its own conversion to UTC for the API call. - zoom_creation_result = create_zoom_meeting( - topic=topic_val, - start_time=start_time_val, # Pass the datetime object - duration=duration_val, - ) +# # # Create Zoom meeting using utility function +# # # The create_zoom_meeting expects start_time as a datetime object +# # # and handles its own conversion to UTC for the API call. +# # zoom_creation_result = create_zoom_meeting( +# # topic=topic_val, +# # start_time=start_time_val, # Pass the datetime object +# # duration=duration_val, +# # ) - if zoom_creation_result["status"] == "success": - zoom_details = zoom_creation_result["meeting_details"] - zoom_meeting_instance = ZoomMeetingDetails.objects.create( - topic=topic_val, - start_time=start_time_val, # Store the original datetime - duration=duration_val, - meeting_id=zoom_details["meeting_id"], - details_url=zoom_details["join_url"], - password=zoom_details.get("password"), # password might be None - status=zoom_creation_result["zoom_gateway_response"].get( - "status", "waiting" - ), - zoom_gateway_response=zoom_creation_result["zoom_gateway_response"], - location_type='Remote', +# # if zoom_creation_result["status"] == "success": +# # zoom_details = zoom_creation_result["meeting_details"] +# # # zoom_meeting_instance = ZoomMeetingDetails.objects.create( +# # # topic=topic_val, +# # # start_time=start_time_val, # Store the original datetime +# # # duration=duration_val, +# # # meeting_id=zoom_details["meeting_id"], +# # # details_url=zoom_details["join_url"], +# # # password=zoom_details.get("password"), # password might be None +# # # status=zoom_creation_result["zoom_gateway_response"].get( +# # # "status", "waiting" +# # # ), +# # # zoom_gateway_response=zoom_creation_result["zoom_gateway_response"], +# # # location_type='Remote', - ) - # Create a ScheduledInterview record - ScheduledInterview.objects.create( - application=application, - job=job, - interview_location=zoom_meeting_instance, - interview_date=start_time_val.date(), - interview_time=start_time_val.time(), - status="scheduled", - ) - messages.success(request, f"Meeting scheduled with {application.name}.") - return redirect("applications_interview_view", slug=job.slug) - else: - messages.error( - request, - f"Failed to create Zoom meeting: {zoom_creation_result['message']}", - ) - # Re-render form with error - return render( - request, - "recruitment/schedule_meeting_form.html", - { - "form": form, - "job": job, - "application": application, - "initial_topic": topic_val, - "initial_start_time": start_time_val.strftime("%Y-%m-%dT%H:%M") - if start_time_val - else "", - "initial_duration": duration_val, - }, - ) - else: - # Form validation errors - return render( - request, - "meetings/schedule_meeting_form.html", - { - "form": form, - "job": job, - "application": application, - "initial_topic": request.POST.get( - "topic", f"Interview: {job.title} with {application.name}" - ), - "initial_start_time": request.POST.get("start_time", ""), - "initial_duration": request.POST.get("duration", 60), - }, - ) - else: # GET request - initial_data = { - "topic": f"Interview: {job.title} with {application.name}", - "start_time": (timezone.now() + timedelta(hours=1)).strftime( - "%Y-%m-%dT%H:%M" - ), # Default to 1 hour from now - "duration": 60, # Default duration - } - form = ZoomMeetingForm(initial=initial_data) - return render( - request, - "meetings/schedule_meeting_form.html", - {"form": form, "job": job, "application": application}, - ) +# ) +# # Create a ScheduledInterview record +# ScheduledInterview.objects.create( +# application=application, +# job=job, +# interview_location=zoom_meeting_instance, +# interview_date=start_time_val.date(), +# interview_time=start_time_val.time(), +# status="scheduled", +# ) +# messages.success(request, f"Meeting scheduled with {application.name}.") +# return redirect("applications_interview_view", slug=job.slug) +# else: +# messages.error( +# request, +# f"Failed to create Zoom meeting: {zoom_creation_result['message']}", +# ) +# # Re-render form with error +# return render( +# request, +# "recruitment/schedule_meeting_form.html", +# { +# "form": form, +# "job": job, +# "application": application, +# "initial_topic": topic_val, +# "initial_start_time": start_time_val.strftime("%Y-%m-%dT%H:%M") +# if start_time_val +# else "", +# "initial_duration": duration_val, +# }, +# ) +# else: +# # Form validation errors +# return render( +# request, +# "meetings/schedule_meeting_form.html", +# { +# "form": form, +# "job": job, +# "application": application, +# "initial_topic": request.POST.get( +# "topic", f"Interview: {job.title} with {application.name}" +# ), +# "initial_start_time": request.POST.get("start_time", ""), +# "initial_duration": request.POST.get("duration", 60), +# }, +# ) +# else: # GET request +# initial_data = { +# "topic": f"Interview: {job.title} with {application.name}", +# "start_time": (timezone.now() + timedelta(hours=1)).strftime( +# "%Y-%m-%dT%H:%M" +# ), # Default to 1 hour from now +# "duration": 60, # Default duration +# } +# form = ZoomMeetingForm(initial=initial_data) +# return render( +# request, +# "meetings/schedule_meeting_form.html", +# {"form": form, "job": job, "application": application}, +# ) from django.core.exceptions import ObjectDoesNotExist @@ -3106,163 +3101,165 @@ def zoom_webhook_view(request): # Meeting Comments Views -@staff_user_required -def add_meeting_comment(request, slug): - """Add a comment to a meeting""" - # from .forms import MeetingCommentForm +# @staff_user_required +# def add_meeting_comment(request, slug): +# """Add a comment to a meeting""" +# # from .forms import MeetingCommentForm - meeting = get_object_or_404(InterviewNote, slug=slug) - print(meeting) +# meeting = get_object_or_404(InterviewNote, slug=slug) +# print(meeting) - if request.method == "POST": - form = InterviewNoteForm(request.POST) - if form.is_valid(): - comment = form.save(commit=False) - comment.meeting = meeting - comment.author = request.user - comment.save() - messages.success(request, "Comment added successfully!") +# if request.method == "POST": +# form = InterviewNoteForm(request.POST) +# if form.is_valid(): +# comment = form.save(commit=False) +# comment.meeting = meeting +# comment.author = request.user +# comment.save() +# messages.success(request, "Comment added successfully!") - # HTMX response - return just the comment section - if "HX-Request" in request.headers: - return render( - request, - "includes/comment_list.html", - { - "comments": meeting.comments.all().order_by("-created_at"), - "meeting": meeting, - }, - ) +# # HTMX response - return just the comment section +# if "HX-Request" in request.headers: +# return render( +# request, +# "includes/comment_list.html", +# { +# "comments": meeting.comments.all().order_by("-created_at"), +# "meeting": meeting, +# }, +# ) - return redirect("meeting_details", slug=slug) - else: - form = InterviewNoteForm() +# return redirect("meeting_details", slug=slug) +# else: +# form = InterviewNoteForm() - context = { - "form": form, - "meeting": meeting, - } +# context = { +# "form": form, +# "meeting": meeting, +# } - # HTMX response - return the comment form - if "HX-Request" in request.headers: - return render(request, "includes/comment_form.html", context) +# # HTMX response - return the comment form +# if "HX-Request" in request.headers: +# return render(request, "includes/comment_form.html", context) - return redirect("meeting_details", slug=slug) +# return redirect("meeting_details", slug=slug) -@staff_user_required -def edit_meeting_comment(request, slug, comment_id): - """Edit a meeting comment""" - meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) - comment = get_object_or_404(InterviewNote, id=comment_id, meeting=meeting) +# @staff_user_required +# def edit_meeting_comment(request, slug, comment_id): +# """Edit a meeting comment""" +# # meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) +# meeting = None#TODO:Update +# comment = get_object_or_404(InterviewNote, id=comment_id, meeting=meeting) - # Check if user is author - if comment.author != request.user and not request.user.is_staff: - messages.error(request, "You can only edit your own comments.") - return redirect("meeting_details", slug=slug) +# # Check if user is author +# if comment.author != request.user and not request.user.is_staff: +# messages.error(request, "You can only edit your own comments.") +# return redirect("meeting_details", slug=slug) - if request.method == "POST": - form = InterviewNoteForm(request.POST, instance=comment) - if form.is_valid(): - comment = form.save() - messages.success(request, "Comment updated successfully!") +# if request.method == "POST": +# form = InterviewNoteForm(request.POST, instance=comment) +# if form.is_valid(): +# comment = form.save() +# messages.success(request, "Comment updated successfully!") - # HTMX response - return just comment section - if "HX-Request" in request.headers: - return render( - request, - "includes/comment_list.html", - { - "comments": meeting.comments.all().order_by("-created_at"), - "meeting": meeting, - }, - ) +# # HTMX response - return just comment section +# if "HX-Request" in request.headers: +# return render( +# request, +# "includes/comment_list.html", +# { +# "comments": meeting.comments.all().order_by("-created_at"), +# "meeting": meeting, +# }, +# ) - return redirect("meeting_details", slug=slug) - else: - form = InterviewNoteForm(instance=comment) +# return redirect("meeting_details", slug=slug) +# else: +# form = InterviewNoteForm(instance=comment) - context = {"form": form, "meeting": meeting, "comment": comment} - return render(request, "includes/edit_comment_form.html", context) +# context = {"form": form, "meeting": meeting, "comment": comment} +# return render(request, "includes/edit_comment_form.html", context) -@staff_user_required -def delete_meeting_comment(request, slug, comment_id): - """Delete a meeting comment""" - meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) - comment = get_object_or_404(InterviewNote, id=comment_id, meeting=meeting) +# @staff_user_required +# def delete_meeting_comment(request, slug, comment_id): +# """Delete a meeting comment""" +# # meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) +# meeting = None#TODO:Update +# comment = get_object_or_404(InterviewNote, id=comment_id, meeting=meeting) - # Check if user is the author - if comment.author != request.user and not request.user.is_staff: - messages.error(request, "You can only delete your own comments.") - return redirect("meeting_details", slug=slug) +# # Check if user is the author +# if comment.author != request.user and not request.user.is_staff: +# messages.error(request, "You can only delete your own comments.") +# return redirect("meeting_details", slug=slug) - if request.method == "POST": - comment.delete() - messages.success(request, "Comment deleted successfully!") +# if request.method == "POST": +# comment.delete() +# messages.success(request, "Comment deleted successfully!") - # HTMX response - return just the comment section - if "HX-Request" in request.headers: - return render( - request, - "includes/comment_list.html", - { - "comments": meeting.comments.all().order_by("-created_at"), - "meeting": meeting, - }, - ) +# # HTMX response - return just the comment section +# if "HX-Request" in request.headers: +# return render( +# request, +# "includes/comment_list.html", +# { +# "comments": meeting.comments.all().order_by("-created_at"), +# "meeting": meeting, +# }, +# ) - return redirect("meeting_details", slug=slug) +# return redirect("meeting_details", slug=slug) - # HTMX response - return the delete confirmation modal - if "HX-Request" in request.headers: - return render( - request, - "includes/delete_comment_form.html", - { - "meeting": meeting, - "comment": comment, - "delete_url": reverse( - "delete_meeting_comment", - kwargs={"slug": slug, "comment_id": comment_id}, - ), - }, - ) +# # HTMX response - return the delete confirmation modal +# if "HX-Request" in request.headers: +# return render( +# request, +# "includes/delete_comment_form.html", +# { +# "meeting": meeting, +# "comment": comment, +# "delete_url": reverse( +# "delete_meeting_comment", +# kwargs={"slug": slug, "comment_id": comment_id}, +# ), +# }, +# ) - return redirect("meeting_details", slug=slug) +# return redirect("meeting_details", slug=slug) -@staff_user_required -def set_meeting_application(request, slug): - meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) - if request.method == "POST" and "HX-Request" not in request.headers: - form = InterviewForm(request.POST) - if form.is_valid(): - candidate = form.save(commit=False) - candidate.zoom_meeting = meeting - candidate.interview_date = meeting.start_time.date() - candidate.interview_time = meeting.start_time.time() - candidate.save() - messages.success(request, "Candidate added successfully!") - return redirect("list_meetings") - job = request.GET.get("job") - form = InterviewForm() +# @staff_user_required +# def set_meeting_application(request, slug): +# meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) +# if request.method == "POST" and "HX-Request" not in request.headers: +# form = InterviewForm(request.POST) +# if form.is_valid(): +# candidate = form.save(commit=False) +# candidate.zoom_meeting = meeting +# candidate.interview_date = meeting.start_time.date() +# candidate.interview_time = meeting.start_time.time() +# candidate.save() +# messages.success(request, "Candidate added successfully!") +# return redirect("list_meetings") +# job = request.GET.get("job") +# form = InterviewForm() - if job: - form.fields["candidate"].queryset = Application.objects.filter(job=job) +# if job: +# form.fields["candidate"].queryset = Application.objects.filter(job=job) - else: - form.fields["candidate"].queryset = Application.objects.none() - form.fields["job"].widget.attrs.update( - { - "hx-get": reverse("set_meeting_application", kwargs={"slug": slug}), - "hx-target": "#div_id_candidate", - "hx-select": "#div_id_candidate", - "hx-swap": "outerHTML", - } - ) - context = {"form": form, "meeting": meeting} - return render(request, "meetings/set_candidate_form.html", context) + # else: + # form.fields["candidate"].queryset = Application.objects.none() + # form.fields["job"].widget.attrs.update( + # { + # "hx-get": reverse("set_meeting_application", kwargs={"slug": slug}), + # "hx-target": "#div_id_candidate", + # "hx-select": "#div_id_candidate", + # "hx-swap": "outerHTML", + # } + # ) + # context = {"form": form, "meeting": meeting} + # return render(request, "meetings/set_candidate_form.html", context) # Hiring Agency CRUD Views @@ -4190,7 +4187,7 @@ def agency_portal_persons_list(request): | Q(last_name__icontains=search_query) | Q(email__icontains=search_query) | Q(phone__icontains=search_query) - + | Q(job__title__icontains=search_query) ) # Filter by stage if provided @@ -5136,6 +5133,161 @@ def portal_logout(request): return redirect("portal_login") +# Interview Creation Views +@staff_user_required +def interview_create_type_selection(request, candidate_slug): + """Show interview type selection page for a candidate""" + candidate = get_object_or_404(Application, slug=candidate_slug) + + # Validate candidate is in Interview stage + if candidate.stage != 'Interview': + messages.error(request, f"Candidate {candidate.name} is not in Interview stage.") + return redirect('candidate_interview_view', slug=candidate.job.slug) + + context = { + 'candidate': candidate, + 'job': candidate.job, + } + return render(request, 'interviews/interview_create_type_selection.html', context) + + +@staff_user_required +def interview_create_remote(request, candidate_slug): + """Create remote interview for a candidate""" + application = get_object_or_404(Application, slug=candidate_slug) + + # Validate candidate is in Interview stage + # if candidate.stage != 'Interview': + # messages.error(request, f"Candidate {candidate.name} is not in Interview stage.") + # return redirect('candidate_interview_view', slug=candidate.job.slug) + + if request.method == 'POST': + form = RemoteInterviewForm(request.POST) + if form.is_valid(): + try: + with transaction.atomic(): + # Create ScheduledInterview record + schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) + async_task( + "recruitment.tasks.create_interview_and_meeting", + application.pk, application.job.pk, schedule.pk, schedule.interview_date,schedule.interview_time, form.cleaned_data['duration'] + ) + # interview.interview_type = 'REMOTE' + # interview.status = 'SCHEDULED' + # interview.save() + + # Create ZoomMeetingDetails record + # from .models import ZoomMeetingDetails + # zoom_meeting = ZoomMeetingDetails.objects.create( + # topic=form.cleaned_data['topic'], + # start_time=timezone.make_aware( + # timezone.datetime.combine( + # form.cleaned_data['interview_date'], + # form.cleaned_data['interview_time'] + # ), + # timezone.get_current_timezone() + # ), + # duration=form.cleaned_data['duration'], + # meeting_id=f"KAUH-{interview.id}-{timezone.now().timestamp()}", + # join_url=f"https://zoom.us/j/{interview.id}", + # password=secrets.token_urlsafe(16), + # status='scheduled' + # ) + + # Link Zoom meeting to interview + # interview.interview_location = zoom_meeting + # interview.save() + + messages.success(request, f"Remote interview scheduled for {application.name}") + return redirect('interview_list') + + except Exception as e: + messages.error(request, f"Error creating remote interview: {str(e)}") + form = RemoteInterviewForm() + form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" + context = { + 'candidate': application, + 'job': application.job, + 'form': form, + } + return render(request, 'interviews/interview_create_remote.html', context) + + +@staff_user_required +def interview_create_onsite(request, candidate_slug): + """Create onsite interview for a candidate""" + candidate = get_object_or_404(Application, slug=candidate_slug) + + # Validate candidate is in Interview stage + # if candidate.stage != 'Interview': + # messages.error(request, f"Candidate {candidate.name} is not in Interview stage.") + # return redirect('candidate_interview_view', slug=candidate.job.slug) + + if request.method == 'POST': + from .models import Interview + + form = OnsiteInterviewForm(request.POST) + if form.is_valid(): + try: + with transaction.atomic(): + interview = Interview.objects.create(topic=form.cleaned_data["topic"], + start_time=form.cleaned_data["interview_date"],room_number=form.cleaned_data["room_number"], + physical_address=form.cleaned_data["physical_address"], + duration=form.cleaned_data["duration"],location_type="Onsite",status="SCHEDULED") + + schedule = ScheduledInterview.objects.create(application=candidate,job=candidate.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) + # Create ScheduledInterview record + # interview = form.save(commit=False) + # interview.interview_type = 'ONSITE' + # interview.status = 'SCHEDULED' + # interview.save() + + # Create OnsiteLocationDetails record + # from .models import OnsiteLocationDetails + # onsite_location = OnsiteLocationDetails.objects.create( + # topic=form.cleaned_data['topic'], + # start_time=timezone.make_aware( + # timezone.datetime.combine( + # form.cleaned_data['interview_date'], + # form.cleaned_data['interview_time'] + # ), + # timezone.get_current_timezone() + # ), + # duration=form.cleaned_data['duration'], + # physical_address=form.cleaned_data['physical_address'], + # room_number=form.cleaned_data.get('room_number', ''), + # location_type='ONSITE', + # status='scheduled' + # ) + + # # Link onsite location to interview + # interview.interview_location = onsite_location + # interview.save() + + messages.success(request, f"Onsite interview scheduled for {candidate.name}") + return redirect('interview_detail', slug=schedule.slug) + + except Exception as e: + messages.error(request, f"Error creating onsite interview: {str(e)}") + else: + # Pre-populate topic + form.initial['topic'] = f"Interview for {candidate.job.title} - {candidate.name}" + + form = OnsiteInterviewForm() + context = { + 'candidate': candidate, + 'job': candidate.job, + 'form': form, + } + return render(request, 'interviews/interview_create_onsite.html', context) + + +def get_interview_list(request, job_slug): + application = Application.objects.get(slug=job_slug) + interviews = ScheduledInterview.objects.filter(application=application).order_by("interview_date","interview_time").select_related('interview') + print(interviews) + return render(request, 'interviews/partials/interview_list.html', {'interviews': interviews, 'application': application}) + @login_required def agency_access_link_deactivate(request, slug): """Deactivate an agency access link""" @@ -5610,7 +5762,58 @@ def application_signup(request, slug): ) -from .forms import InterviewParticpantsForm +# Interview Views +@staff_user_required +def interview_list(request): + """List all interviews with filtering and pagination""" + interviews = ScheduledInterview.objects.select_related( + 'application','application__person', 'job', + ).order_by('-interview_date', '-interview_time') + + # Get filter parameters + status_filter = request.GET.get('status', '') + job_filter = request.GET.get('job', '') + search_query = request.GET.get('q', '') + + # Apply filters + if status_filter: + interviews = interviews.filter(status=status_filter) + if job_filter: + interviews = interviews.filter(job__title__icontains=job_filter) + if search_query: + interviews = interviews.filter( + Q(application__person__first_name__icontains=search_query) | + Q(application__person__last_name__icontains=search_query) | + Q(job__title__icontains=search_query) + ) + + # Pagination + paginator = Paginator(interviews, 20) # Show 20 interviews per page + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + context = { + 'page_obj': page_obj, + 'status_filter': status_filter, + 'job_filter': job_filter, + 'search_query': search_query, + 'interviews': interviews, + } + return render(request, 'interviews/interview_list.html', context) + + +@staff_user_required +def interview_detail(request, slug): + """View details of a specific interview""" + interview = get_object_or_404(ScheduledInterview, slug=slug) + + context = { + 'interview': interview, + } + return render(request, 'interviews/interview_detail.html', context) + + +# from .forms import InterviewParticpantsFormreschedule_meeting_for_candidate # def create_interview_participants(request, slug): @@ -5634,269 +5837,270 @@ from .forms import InterviewParticpantsForm # request, "interviews/interview_participants_form.html", {"form": form} # ) -def create_interview_participants(request, slug): - """ - Manage participants for a ScheduledInterview. - Uses interview_pk because ScheduledInterview has no slug. - """ - schedule_interview = get_object_or_404(ScheduledInterview, slug=slug) +# def create_interview_participants(request, slug): +# """ +# Manage participants for a ScheduledInterview. +# Uses interview_pk because ScheduledInterview has no slug. +# """ +# schedule_interview = get_object_or_404(ScheduledInterview, slug=slug) - # Get the slug from the related InterviewLocation (the "meeting") - meeting_slug = schedule_interview.interview_location.slug # ✅ Correct +# # Get the slug from the related InterviewLocation (the "meeting") +# meeting_slug = schedule_interview.interview_location.slug # ✅ Correct - if request.method == "POST": - form = InterviewParticpantsForm(request.POST, instance=schedule_interview) - if form.is_valid(): - form.save() # No need for commit=False — it's not a create, just update - messages.success(request, "Participants updated successfully.") - return redirect("meeting_details", slug=meeting_slug) - else: - form = InterviewParticpantsForm(instance=schedule_interview) +# if request.method == "POST": +# form = InterviewParticpantsForm(request.POST, instance=schedule_interview) +# if form.is_valid(): +# form.save() # No need for commit=False — it's not a create, just update +# messages.success(request, "Participants updated successfully.") +# return redirect("meeting_details", slug=meeting_slug) +# else: +# form = InterviewParticpantsForm(instance=schedule_interview) - return render( - request, - "interviews/interview_participants_form.html", - {"form": form, "interview": schedule_interview} - ) +# return render( +# request, +# "interviews/interview_participants_form.html", +# {"form": form, "interview": schedule_interview} +# ) -from django.core.mail import send_mail +# from django.core.mail import send_mail -def send_interview_email(request, slug): - from .email_service import send_bulk_email +# def send_interview_email(request, slug): +# from .email_service import send_bulk_email - interview = get_object_or_404(ScheduledInterview, slug=slug) +# interview = get_object_or_404(ScheduledInterview, slug=slug) - # 2. Retrieve the required data for the form's constructor - candidate = interview.application - job = interview.job - meeting = interview.interview_location - participants = list(interview.participants.all()) + list( - interview.system_users.all() - ) - external_participants = list(interview.participants.all()) - system_participants = list(interview.system_users.all()) +# # 2. Retrieve the required data for the form's constructor +# candidate = interview.application +# job = interview.job +# meeting = interview.interview_location +# participants = list(interview.participants.all()) + list( +# interview.system_users.all() +# ) +# external_participants = list(interview.participants.all()) +# system_participants = list(interview.system_users.all()) - participant_emails = [p.email for p in participants if hasattr(p, "email")] - print(participant_emails) - total_recipients = 1 + len(participant_emails) +# participant_emails = [p.email for p in participants if hasattr(p, "email")] +# print(participant_emails) +# total_recipients = 1 + len(participant_emails) - # --- POST REQUEST HANDLING --- - if request.method == "POST": - form = InterviewEmailForm( - request.POST, - candidate=candidate, - external_participants=external_participants, - system_participants=system_participants, - meeting=meeting, - job=job, - ) +# # --- POST REQUEST HANDLING --- +# if request.method == "POST": +# form = InterviewEmailForm( +# request.POST, +# candidate=candidate, +# external_participants=external_participants, +# system_participants=system_participants, +# meeting=meeting, +# job=job, +# ) - if form.is_valid(): - # 4. Extract cleaned data - subject = form.cleaned_data["subject"] - msg_candidate = form.cleaned_data["message_for_candidate"] - msg_agency = form.cleaned_data["message_for_agency"] - msg_participants = form.cleaned_data["message_for_participants"] +# if form.is_valid(): +# # 4. Extract cleaned data +# subject = form.cleaned_data["subject"] +# msg_candidate = form.cleaned_data["message_for_candidate"] +# msg_agency = form.cleaned_data["message_for_agency"] +# msg_participants = form.cleaned_data["message_for_participants"] - # --- SEND EMAILS Candidate or agency--- - if candidate.belong_to_an_agency: - email=candidate.hiring_agency.email - print(email) - send_mail( - subject, - msg_agency, - settings.DEFAULT_FROM_EMAIL, - [candidate.hiring_agency.email], - fail_silently=False, - ) - else: - send_mail( - subject, - msg_candidate, - settings.DEFAULT_FROM_EMAIL, - [candidate.person.email], - fail_silently=False, - ) +# # --- SEND EMAILS Candidate or agency--- +# if candidate.belong_to_an_agency: +# email=candidate.hiring_agency.email +# print(email) +# send_mail( +# subject, +# msg_agency, +# settings.DEFAULT_FROM_EMAIL, +# [candidate.hiring_agency.email], +# fail_silently=False, +# ) +# else: +# send_mail( +# subject, +# msg_candidate, +# settings.DEFAULT_FROM_EMAIL, +# [candidate.person.email], +# fail_silently=False, +# ) - email_result = send_bulk_email( - subject=subject, - message=msg_participants, - recipient_list=participant_emails, - request=request, - attachments=None, - async_task_=True, # Changed to False to avoid pickle issues, - from_interview=True, - job=job +# email_result = send_bulk_email( +# subject=subject, +# message=msg_participants, +# recipient_list=participant_emails, +# request=request, +# attachments=None, +# async_task_=True, # Changed to False to avoid pickle issues, +# from_interview=True, +# job=job - ) +# ) - if email_result["success"]: - # Create Message records for each participant after successful email send - messages_created = 0 - for participant in participants: - if hasattr(participant, 'user') and participant.user: - try: - Message.objects.create( - sender=request.user, - recipient=participant.user, - subject=subject, - content=msg_participants, - job=job, - message_type='email', - is_email_sent=True, - email_address=participant.email if hasattr(participant, 'email') else '' - ) - messages_created += 1 - except Exception as e: - # Log error but don't fail the entire process - print(f"Error creating message for {participant.email if hasattr(participant, 'email') else participant}: {e}") +# if email_result["success"]: +# # Create Message records for each participant after successful email send +# messages_created = 0 +# for participant in participants: +# if hasattr(participant, 'user') and participant.user: +# try: +# Message.objects.create( +# sender=request.user, +# recipient=participant.user, +# subject=subject, +# content=msg_participants, +# job=job, +# message_type='email', +# is_email_sent=True, +# email_address=participant.email if hasattr(participant, 'email') else '' +# ) +# messages_created += 1 +# except Exception as e: +# # Log error but don't fail the entire process +# print(f"Error creating message for {participant.email if hasattr(participant, 'email') else participant}: {e}") - messages.success( - request, - f"Email will be sent shortly to {total_recipients} recipient(s).", - ) +# messages.success( +# request, +# f"Email will be sent shortly to {total_recipients} recipient(s).", +# ) - return redirect("list_meetings") - else: - messages.error( - request, - f"Failed to send email: {email_result.get('message', 'Unknown error')}", - ) - return redirect("list_meetings") - else: +# return redirect("list_meetings") +# else: +# messages.error( +# request, +# f"Failed to send email: {email_result.get('message', 'Unknown error')}", +# ) +# return redirect("list_meetings") +# else: - error_msg = "Failed to send email. Please check the form for errors." - print(form.errors) - messages.error(request, error_msg) - return redirect("meeting_details", slug=meeting.slug) - return redirect("meeting_details", slug=meeting.slug) +# error_msg = "Failed to send email. Please check the form for errors." +# print(form.errors) +# messages.error(request, error_msg) +# return redirect("meeting_details", slug=meeting.slug) +# return redirect("meeting_details", slug=meeting.slug) -def schedule_interview_location_form(request,slug): - schedule=get_object_or_404(InterviewSchedule,slug=slug) - if request.method=='POST': - form=InterviewScheduleLocationForm(request.POST,instance=schedule) - form.save() - return redirect('list_meetings') - else: - form=InterviewScheduleLocationForm(instance=schedule) - return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule}) +#TODO:Update +# def schedule_interview_location_form(request,slug): +# schedule=get_object_or_404(BulkInterviewTemplate,slug=slug) +# if request.method=='POST': +# form=BulkInterviewTemplateLocationForm(request.POST,instance=schedule) +# form.save() +# return redirect('list_meetings') +# else: +# form=BulkInterviewTemplateLocationForm(instance=schedule) +# return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule}) -class MeetingListView(ListView): - """ - A unified view to list both Remote and Onsite Scheduled Interviews. - """ - model = ScheduledInterview - template_name = "meetings/list_meetings.html" - context_object_name = "meetings" - paginate_by = 100 +# class MeetingListView(ListView): +# """ +# A unified view to list both Remote and Onsite Scheduled Interviews. +# """ +# model = ScheduledInterview +# template_name = "meetings/list_meetings.html" +# context_object_name = "meetings" +# paginate_by = 100 - def get_queryset(self): - # Start with a base queryset, ensuring an InterviewLocation link exists. - queryset = super().get_queryset().filter(interview_location__isnull=False).select_related( - 'interview_location', - 'job', - 'application__person', - 'application', - ).prefetch_related( - 'interview_location__zoommeetingdetails', - 'interview_location__onsitelocationdetails', - ) +# def get_queryset(self): +# # Start with a base queryset, ensuring an InterviewLocation link exists. +# queryset = super().get_queryset().filter(interview_location__isnull=False).select_related( +# 'interview_location', +# 'job', +# 'application__person', +# 'application', +# ).prefetch_related( +# # 'interview_location__zoommeetingdetails', +# # 'interview_location__onsitelocationdetails', +# ) - # Note: Printing the queryset here can consume memory for large sets. +# # Note: Printing the queryset here can consume memory for large sets. - # Get filters from GET request - search_query = self.request.GET.get("q") - status_filter = self.request.GET.get("status") - candidate_name_filter = self.request.GET.get("candidate_name") - type_filter = self.request.GET.get("type") - print(type_filter) +# # Get filters from GET request +# search_query = self.request.GET.get("q") +# status_filter = self.request.GET.get("status") +# candidate_name_filter = self.request.GET.get("candidate_name") +# type_filter = self.request.GET.get("type") +# print(type_filter) - # 2. Type Filter: Filter based on the base InterviewLocation's type - if type_filter: - # Use .title() to handle case variations from URL (e.g., 'remote' -> 'Remote') - normalized_type = type_filter.title() +# # 2. Type Filter: Filter based on the base InterviewLocation's type +# if type_filter: +# # Use .title() to handle case variations from URL (e.g., 'remote' -> 'Remote') +# normalized_type = type_filter.title() - # Assuming InterviewLocation.LocationType is accessible/defined - if normalized_type in ['Remote', 'Onsite']: - queryset = queryset.filter(interview_location__location_type=normalized_type) +# # Assuming InterviewLocation.LocationType is accessible/defined +# if normalized_type in ['Remote', 'Onsite']: +# queryset = queryset.filter(interview_location__location_type=normalized_type) - # 3. Search by Topic (stored on InterviewLocation) - if search_query: - queryset = queryset.filter(interview_location__topic__icontains=search_query) +# # 3. Search by Topic (stored on InterviewLocation) +# if search_query: +# queryset = queryset.filter(interview_location__topic__icontains=search_query) - # 4. Status Filter - if status_filter: - queryset = queryset.filter(status=status_filter) +# # 4. Status Filter +# if status_filter: +# queryset = queryset.filter(status=status_filter) - # 5. Candidate Name Filter - if candidate_name_filter: - queryset = queryset.filter( - Q(application__person__first_name__icontains=candidate_name_filter) | - Q(application__person__last_name__icontains=candidate_name_filter) - ) +# # 5. Candidate Name Filter +# if candidate_name_filter: +# queryset = queryset.filter( +# Q(application__person__first_name__icontains=candidate_name_filter) | +# Q(application__person__last_name__icontains=candidate_name_filter) +# ) - return queryset.order_by("-interview_date", "-interview_time") +# return queryset.order_by("-interview_date", "-interview_time") - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) - # Pass filters back to the template for retention - context["search_query"] = self.request.GET.get("q", "") - context["status_filter"] = self.request.GET.get("status", "") - context["candidate_name_filter"] = self.request.GET.get("candidate_name", "") - context["type_filter"] = self.request.GET.get("type", "") +# # Pass filters back to the template for retention +# context["search_query"] = self.request.GET.get("q", "") +# context["status_filter"] = self.request.GET.get("status", "") +# context["candidate_name_filter"] = self.request.GET.get("candidate_name", "") +# context["type_filter"] = self.request.GET.get("type", "") - # CORRECTED: Pass the status choices from the model class for the filter dropdown - context["status_choices"] = self.model.InterviewStatus.choices +# # CORRECTED: Pass the status choices from the model class for the filter dropdown +# context["status_choices"] = self.model.InterviewStatus.choices - meetings_data = [] +# meetings_data = [] - for interview in context.get(self.context_object_name, []): - location = interview.interview_location - details = None +# for interview in context.get(self.context_object_name, []): +# location = interview.interview_location +# details = None - if not location: - continue +# if not location: +# continue - # Determine and fetch the CONCRETE details object (prefetched) - if location.location_type == location.LocationType.REMOTE: - details = getattr(location, 'zoommeetingdetails', None) - elif location.location_type == location.LocationType.ONSITE: - details = getattr(location, 'onsitelocationdetails', None) +# # Determine and fetch the CONCRETE details object (prefetched) +# if location.location_type == location.LocationType.REMOTE: +# details = getattr(location, 'zoommeetingdetails', None) +# elif location.location_type == location.LocationType.ONSITE: +# details = getattr(location, 'onsitelocationdetails', None) - # Combine date and time for template display/sorting - start_datetime = None - if interview.interview_date and interview.interview_time: - start_datetime = datetime.combine(interview.interview_date, interview.interview_time) +# # Combine date and time for template display/sorting +# start_datetime = None +# if interview.interview_date and interview.interview_time: +# start_datetime = datetime.combine(interview.interview_date, interview.interview_time) - # SUCCESS: Build the data dictionary - meetings_data.append({ - 'interview': interview, - 'location': location, - 'details': details, - 'type': location.location_type, - 'topic': location.topic, - 'slug': interview.slug, - 'start_time': start_datetime, # Combined datetime object - # Duration should ideally be on ScheduledInterview or fetched from details - 'duration': getattr(details, 'duration', 'N/A'), - # Use details.join_url and fallback to None, if Remote - 'join_url': getattr(details, 'join_url', None) if location.location_type == location.LocationType.REMOTE else None, - 'meeting_id': getattr(details, 'meeting_id', None), - # Use the primary status from the ScheduledInterview record - 'status': interview.status, - }) +# # SUCCESS: Build the data dictionary +# meetings_data.append({ +# 'interview': interview, +# 'location': location, +# 'details': details, +# 'type': location.location_type, +# 'topic': location.topic, +# 'slug': interview.slug, +# 'start_time': start_datetime, # Combined datetime object +# # Duration should ideally be on ScheduledInterview or fetched from details +# 'duration': getattr(details, 'duration', 'N/A'), +# # Use details.join_url and fallback to None, if Remote +# 'join_url': getattr(details, 'join_url', None) if location.location_type == location.LocationType.REMOTE else None, +# 'meeting_id': getattr(details, 'meeting_id', None), +# # Use the primary status from the ScheduledInterview record +# 'status': interview.status, +# }) - context["meetings_data"] = meetings_data +# context["meetings_data"] = meetings_data - return context +# return context # class MeetingListView(ListView): @@ -5920,370 +6124,370 @@ class MeetingListView(ListView): # return queryset -def reschedule_onsite_meeting(request, slug, candidate_id, meeting_id): - """Handles the rescheduling of an Onsite Interview (updates OnsiteLocationDetails).""" - job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_id) +# def reschedule_onsite_meeting(request, slug, candidate_id, meeting_id): +# """Handles the rescheduling of an Onsite Interview (updates OnsiteLocationDetails).""" +# job = get_object_or_404(JobPosting, slug=slug) +# candidate = get_object_or_404(Application, pk=candidate_id) - # Fetch the OnsiteLocationDetails instance, ensuring it belongs to this candidate. - # We use the reverse relationship: onsitelocationdetails -> interviewlocation -> scheduledinterview -> application - # The 'interviewlocation_ptr' is the foreign key field name if OnsiteLocationDetails is a proxy/multi-table inheritance model. - onsite_meeting = get_object_or_404( - OnsiteLocationDetails, - pk=meeting_id, - # Correct filter: Use the reverse link through the ScheduledInterview model. - # This assumes your ScheduledInterview model links back to a generic InterviewLocation base. - interviewlocation_ptr__scheduled_interview__application=candidate - ) +# # Fetch the OnsiteLocationDetails instance, ensuring it belongs to this candidate. +# # We use the reverse relationship: onsitelocationdetails -> interviewlocation -> scheduledinterview -> application +# # The 'interviewlocation_ptr' is the foreign key field name if OnsiteLocationDetails is a proxy/multi-table inheritance model. +# onsite_meeting = get_object_or_404( +# OnsiteLocationDetails, +# pk=meeting_id, +# # Correct filter: Use the reverse link through the ScheduledInterview model. +# # This assumes your ScheduledInterview model links back to a generic InterviewLocation base. +# interviewlocation_ptr__scheduled_interview__application=candidate +# ) - if request.method == 'POST': - form = OnsiteReshuduleForm(request.POST, instance=onsite_meeting) +# if request.method == 'POST': +# form = OnsiteReshuduleForm(request.POST, instance=onsite_meeting) - if form.is_valid(): - instance = form.save(commit=False) +# if form.is_valid(): +# instance = form.save(commit=False) - if instance.start_time < timezone.now(): - messages.error(request, "Start time must be in the future for rescheduling.") - return render(request, "meetings/reschedule_onsite.html", {"form": form, "job": job, "candidate": candidate, "meeting": onsite_meeting}) +# if instance.start_time < timezone.now(): +# messages.error(request, "Start time must be in the future for rescheduling.") +# return render(request, "meetings/reschedule_onsite.html", {"form": form, "job": job, "candidate": candidate, "meeting": onsite_meeting}) - # Update parent status - try: - # Retrieve the ScheduledInterview instance via the reverse relationship - scheduled_interview = ScheduledInterview.objects.get( - interview_location=instance.interviewlocation_ptr # Use the base model FK - ) - scheduled_interview.status = ScheduledInterview.InterviewStatus.SCHEDULED - scheduled_interview.save() - except ScheduledInterview.DoesNotExist: - messages.warning(request, "Parent schedule record not found. Status not updated.") +# # Update parent status +# try: +# # Retrieve the ScheduledInterview instance via the reverse relationship +# scheduled_interview = ScheduledInterview.objects.get( +# interview_location=instance.interviewlocation_ptr # Use the base model FK +# ) +# scheduled_interview.status = ScheduledInterview.InterviewStatus.SCHEDULED +# scheduled_interview.save() +# except ScheduledInterview.DoesNotExist: +# messages.warning(request, "Parent schedule record not found. Status not updated.") - instance.save() - messages.success(request, "Onsite meeting successfully rescheduled! ✅") +# instance.save() +# messages.success(request, "Onsite meeting successfully rescheduled! ✅") - return redirect(reverse("applications_interview_view", kwargs={'slug': job.slug})) + # return redirect(reverse("applications_interview_view", kwargs={'slug': job.slug})) - else: - form = OnsiteReshuduleForm(instance=onsite_meeting) +# else: +# form = OnsiteReshuduleForm(instance=onsite_meeting) - context = { - "form": form, - "job": job, - "candidate": candidate, - "meeting": onsite_meeting - } - return render(request, "meetings/reschedule_onsite_meeting.html", context) +# context = { +# "form": form, +# "job": job, +# "candidate": candidate, +# "meeting": onsite_meeting +# } +# return render(request, "meetings/reschedule_onsite_meeting.html", context) # recruitment/views.py -@staff_user_required -def delete_onsite_meeting_for_application(request, slug, candidate_pk, meeting_id): - """ - Deletes a specific Onsite Location Details instance. - This does not require an external API call. - """ - job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_pk) - - # Target the specific Onsite meeting details instance - meeting = get_object_or_404(OnsiteLocationDetails, pk=meeting_id) - - if request.method == "POST": - # Delete the local Django object. - # This deletes the base InterviewLocation and updates the ScheduledInterview FK. - meeting.delete() - messages.success(request, f"Onsite meeting for {candidate.name} deleted successfully.") - - return redirect(reverse("applications_interview_view", kwargs={"slug": job.slug})) - - context = { - "job": job, - "candidate": candidate, - "meeting": meeting, - "location_type": "Onsite", - "delete_url": reverse( - "delete_onsite_meeting_for_application", # Use the specific new URL name - kwargs={ - "slug": job.slug, - "candidate_pk": candidate_pk, - "meeting_id": meeting_id, - }, - ), - } - return render(request, "meetings/delete_meeting_form.html", context) - - - -def schedule_onsite_meeting_for_application(request, slug, candidate_pk): - """ - Handles scheduling a NEW Onsite Interview for a candidate using OnsiteScheduleForm. - """ - job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_pk) - - action_url = reverse('schedule_onsite_meeting_for_application', - kwargs={'slug': job.slug, 'candidate_pk': candidate.pk}) - - if request.method == 'POST': - # Use the new form - form = OnsiteScheduleForm(request.POST) - if form.is_valid(): - - cleaned_data = form.cleaned_data - - # 1. Create OnsiteLocationDetails - onsite_loc = OnsiteLocationDetails( - topic=cleaned_data['topic'], - physical_address=cleaned_data['physical_address'], - room_number=cleaned_data['room_number'], - start_time=cleaned_data['start_time'], - duration=cleaned_data['duration'], - status=OnsiteLocationDetails.Status.WAITING, - location_type=InterviewLocation.LocationType.ONSITE, - ) - onsite_loc.save() - - # 2. Extract Date and Time - interview_date = cleaned_data['start_time'].date() - interview_time = cleaned_data['start_time'].time() - - # 3. Create ScheduledInterview linked to the new location - # Use cleaned_data['application'] and cleaned_data['job'] from the form - ScheduledInterview.objects.create( - application=cleaned_data['application'], - job=cleaned_data['job'], - interview_location=onsite_loc, - interview_date=interview_date, - interview_time=interview_time, - status=ScheduledInterview.InterviewStatus.SCHEDULED, - ) - - messages.success(request, "Onsite interview scheduled successfully. ✅") - return redirect(reverse("applications_interview_view", kwargs={'slug': job.slug})) - - else: - # GET Request: Initialize the hidden fields with the correct objects - initial_data = { - 'application': candidate, # Pass the object itself for ModelChoiceField - 'job': job, # Pass the object itself for ModelChoiceField - } - # Use the new form - form = OnsiteScheduleForm(initial=initial_data) - - context = { - "form": form, - "job": job, - "candidate": candidate, - "action_url": action_url, - } - - return render(request, "meetings/schedule_onsite_meeting_form.html", context) - - - -from django.http import Http404 - - -def meeting_details(request, slug): - # Fetch the meeting (InterviewLocation or subclass) by slug - meeting = get_object_or_404( - InterviewLocation.objects.select_related( - 'scheduled_interview__application__person', - 'scheduled_interview__job', - 'zoommeetingdetails', - 'onsitelocationdetails', - ).prefetch_related( - 'scheduled_interview__participants', - 'scheduled_interview__system_users', - 'scheduled_interview__notes', - ), - slug=slug - ) - - try: - interview = meeting.scheduled_interview - except ScheduledInterview.DoesNotExist: - raise Http404("No interview is associated with this meeting.") - - candidate = interview.application - job = interview.job - - external_participants = interview.participants.all() - system_participants = interview.system_users.all() - total_participants = external_participants.count() + system_participants.count() - - # Forms for modals - participant_form = InterviewParticpantsForm(instance=interview) - - - email_form = InterviewEmailForm( - candidate=candidate, - external_participants=external_participants, # QuerySet of Participants - system_participants=system_participants, # QuerySet of Users - meeting=meeting, # ← This is InterviewLocation (e.g., ZoomMeetingDetails) - job=job, - ) - - context = { - 'meeting': meeting, - 'interview': interview, - 'candidate': candidate, - 'job': job, - 'external_participants': external_participants, - 'system_participants': system_participants, - 'total_participants': total_participants, - 'form': participant_form, - 'email_form': email_form, - } - - return render(request, 'interviews/detail_interview.html', context) - - -@login_required -def send_application_invitation(request, slug): - """Send invitation email to the candidate""" - meeting = get_object_or_404(InterviewLocation, slug=slug) - - try: - interview = meeting.scheduled_interview - except ScheduledInterview.DoesNotExist: - raise Http404("No interview is associated with this meeting.") - - candidate = interview.application - job = interview.job - - if request.method == 'POST': - try: - from django.core.mail import send_mail - from django.conf import settings - - # Simple email content - subject = f"Interview Invitation - {job.title}" - message = f""" -Dear {candidate.person.first_name} {candidate.person.last_name}, - -You are invited for an interview for the position of {job.title}. - -Meeting Details: -- Date: {interview.interview_date} -- Time: {interview.interview_time} -- Duration: {meeting.duration or 60} minutes -""" - - # Add join URL if it's a Zoom meeting - if hasattr(meeting, 'zoommeetingdetails') and meeting.zoommeetingdetails.join_url: - message += f"- Join URL: {meeting.zoommeetingdetails.join_url}\n" - - # Add physical address if it's an onsite meeting - if hasattr(meeting, 'onsitelocationdetails') and meeting.onsitelocationdetails.physical_address: - message += f"- Location: {meeting.onsitelocationdetails.physical_address}\n" - if meeting.onsitelocationdetails.room_number: - message += f"- Room: {meeting.onsitelocationdetails.room_number}\n" - - message += """ -Please confirm your attendance. - -Best regards, -KAAUH Recruitment Team -""" - - # Send email - send_mail( - subject, - message, - settings.DEFAULT_FROM_EMAIL, - [candidate.person.email], - fail_silently=False, - ) - - messages.success(request, f"Invitation email sent to {candidate.person.email}") - - except Exception as e: - messages.error(request, f"Failed to send invitation email: {str(e)}") - - return redirect('meeting_details', slug=slug) - - -@login_required -def send_participants_invitation(request, slug): - """Send invitation email to all participants""" - meeting = get_object_or_404(InterviewLocation, slug=slug) - - try: - interview = meeting.scheduled_interview - except ScheduledInterview.DoesNotExist: - raise Http404("No interview is associated with this meeting.") - - candidate = interview.application - job = interview.job - - if request.method == 'POST': - try: - from django.core.mail import send_mail - from django.conf import settings - - # Get all participants - participants = list(interview.participants.all()) - system_users = list(interview.system_users.all()) - all_participants = participants + system_users - - if not all_participants: - messages.warning(request, "No participants found to send invitation to.") - return redirect('meeting_details', slug=slug) - - # Simple email content - subject = f"Interview Invitation - {job.title} with {candidate.person.first_name} {candidate.person.last_name}" - message = f""" -Dear Team Member, - -You are invited to participate in an interview session. - -Interview Details: -- Candidate: {candidate.person.first_name} {candidate.person.last_name} -- Position: {job.title} -- Date: {interview.interview_date} -- Time: {interview.interview_time} -- Duration: {meeting.duration or 60} minutes -""" - - # Add join URL if it's a Zoom meeting - if hasattr(meeting, 'zoommeetingdetails') and meeting.zoommeetingdetails.join_url: - message += f"- Join URL: {meeting.zoommeetingdetails.join_url}\n" - - # Add physical address if it's an onsite meeting - if hasattr(meeting, 'onsitelocationdetails') and meeting.onsitelocationdetails.physical_address: - message += f"- Location: {meeting.onsitelocationdetails.physical_address}\n" - if meeting.onsitelocationdetails.room_number: - message += f"- Room: {meeting.onsitelocationdetails.room_number}\n" - - message += """ -Please confirm your availability. - -Best regards, -KAAUH Recruitment Team -""" - - # Get email addresses of all participants - recipient_emails = [] - for participant in all_participants: - if hasattr(participant, 'email') and participant.email: - recipient_emails.append(participant.email) - - if recipient_emails: - # Send email to all participants - send_mail( - subject, - message, - settings.DEFAULT_FROM_EMAIL, - recipient_emails, - fail_silently=False, - ) - - messages.success(request, f"Invitation emails sent to {len(recipient_emails)} participants") - else: - messages.warning(request, "No valid email addresses found for participants.") - - except Exception as e: - messages.error(request, f"Failed to send invitation emails: {str(e)}") - - return redirect('meeting_details', slug=slug) +# @staff_user_required +# def delete_onsite_meeting_for_application(request, slug, candidate_pk, meeting_id): +# """ +# Deletes a specific Onsite Location Details instance. +# This does not require an external API call. +# """ +# job = get_object_or_404(JobPosting, slug=slug) +# candidate = get_object_or_404(Application, pk=candidate_pk) + +# # Target the specific Onsite meeting details instance +# meeting = get_object_or_404(OnsiteLocationDetails, pk=meeting_id) + +# if request.method == "POST": +# # Delete the local Django object. +# # This deletes the base InterviewLocation and updates the ScheduledInterview FK. +# meeting.delete() +# messages.success(request, f"Onsite meeting for {candidate.name} deleted successfully.") + + # return redirect(reverse("applications_interview_view", kwargs={"slug": job.slug})) + + # context = { + # "job": job, + # "candidate": candidate, + # "meeting": meeting, + # "location_type": "Onsite", + # "delete_url": reverse( + # "delete_onsite_meeting_for_application", # Use the specific new URL name + # kwargs={ + # "slug": job.slug, + # "candidate_pk": candidate_pk, + # "meeting_id": meeting_id, + # }, + # ), + # } + # return render(request, "meetings/delete_meeting_form.html", context) + + + +# def schedule_onsite_meeting_for_application(request, slug, candidate_pk): +# """ +# Handles scheduling a NEW Onsite Interview for a candidate using OnsiteScheduleForm. +# """ +# job = get_object_or_404(JobPosting, slug=slug) +# candidate = get_object_or_404(Application, pk=candidate_pk) + + # action_url = reverse('schedule_onsite_meeting_for_application', + # kwargs={'slug': job.slug, 'candidate_pk': candidate.pk}) + +# if request.method == 'POST': +# # Use the new form +# form = OnsiteScheduleForm(request.POST) +# if form.is_valid(): + +# cleaned_data = form.cleaned_data + +# # 1. Create OnsiteLocationDetails +# onsite_loc = OnsiteLocationDetails( +# topic=cleaned_data['topic'], +# physical_address=cleaned_data['physical_address'], +# room_number=cleaned_data['room_number'], +# start_time=cleaned_data['start_time'], +# duration=cleaned_data['duration'], +# status=OnsiteLocationDetails.Status.WAITING, +# location_type=InterviewLocation.LocationType.ONSITE, +# ) +# onsite_loc.save() + +# # 2. Extract Date and Time +# interview_date = cleaned_data['start_time'].date() +# interview_time = cleaned_data['start_time'].time() + +# # 3. Create ScheduledInterview linked to the new location +# # Use cleaned_data['application'] and cleaned_data['job'] from the form +# ScheduledInterview.objects.create( +# application=cleaned_data['application'], +# job=cleaned_data['job'], +# interview_location=onsite_loc, +# interview_date=interview_date, +# interview_time=interview_time, +# status=ScheduledInterview.InterviewStatus.SCHEDULED, +# ) + + # messages.success(request, "Onsite interview scheduled successfully. ✅") + # return redirect(reverse("applications_interview_view", kwargs={'slug': job.slug})) + +# else: +# # GET Request: Initialize the hidden fields with the correct objects +# initial_data = { +# 'application': candidate, # Pass the object itself for ModelChoiceField +# 'job': job, # Pass the object itself for ModelChoiceField +# } +# # Use the new form +# form = OnsiteScheduleForm(initial=initial_data) + +# context = { +# "form": form, +# "job": job, +# "candidate": candidate, +# "action_url": action_url, +# } + +# return render(request, "meetings/schedule_onsite_meeting_form.html", context) + + + +# from django.http import Http404 + + +# def meeting_details(request, slug): +# # Fetch the meeting (InterviewLocation or subclass) by slug +# meeting = get_object_or_404( +# InterviewLocation.objects.select_related( +# 'scheduled_interview__application__person', +# 'scheduled_interview__job', +# 'zoommeetingdetails', +# 'onsitelocationdetails', +# ).prefetch_related( +# 'scheduled_interview__participants', +# 'scheduled_interview__system_users', +# 'scheduled_interview__notes', +# ), +# slug=slug +# ) + +# try: +# interview = meeting.scheduled_interview +# except ScheduledInterview.DoesNotExist: +# raise Http404("No interview is associated with this meeting.") + +# candidate = interview.application +# job = interview.job + +# external_participants = interview.participants.all() +# system_participants = interview.system_users.all() +# total_participants = external_participants.count() + system_participants.count() + +# # Forms for modals +# participant_form = InterviewParticpantsForm(instance=interview) + + +# email_form = InterviewEmailForm( +# candidate=candidate, +# external_participants=external_participants, # QuerySet of Participants +# system_participants=system_participants, # QuerySet of Users +# meeting=meeting, # ← This is InterviewLocation (e.g., ZoomMeetingDetails) +# job=job, +# ) + +# context = { +# 'meeting': meeting, +# 'interview': interview, +# 'candidate': candidate, +# 'job': job, +# 'external_participants': external_participants, +# 'system_participants': system_participants, +# 'total_participants': total_participants, +# 'form': participant_form, +# 'email_form': email_form, +# } + +# return render(request, 'interviews/detail_interview.html', context) + + +# @login_required +# def send_application_invitation(request, slug): +# """Send invitation email to the candidate""" +# meeting = get_object_or_404(InterviewLocation, slug=slug) + +# try: +# interview = meeting.scheduled_interview +# except ScheduledInterview.DoesNotExist: +# raise Http404("No interview is associated with this meeting.") + +# candidate = interview.application +# job = interview.job + +# if request.method == 'POST': +# try: +# from django.core.mail import send_mail +# from django.conf import settings + +# # Simple email content +# subject = f"Interview Invitation - {job.title}" +# message = f""" +# Dear {candidate.person.first_name} {candidate.person.last_name}, + +# You are invited for an interview for the position of {job.title}. + +# Meeting Details: +# - Date: {interview.interview_date} +# - Time: {interview.interview_time} +# - Duration: {meeting.duration or 60} minutes +# """ + +# # Add join URL if it's a Zoom meeting#TODO:Update +# if hasattr(meeting, 'zoommeetingdetails') and meeting.zoommeetingdetails.join_url: +# message += f"- Join URL: {meeting.zoommeetingdetails.join_url}\n" + +# # Add physical address if it's an onsite meeting +# if hasattr(meeting, 'onsitelocationdetails') and meeting.onsitelocationdetails.physical_address: +# message += f"- Location: {meeting.onsitelocationdetails.physical_address}\n" +# if meeting.onsitelocationdetails.room_number: +# message += f"- Room: {meeting.onsitelocationdetails.room_number}\n" + +# message += """ +# Please confirm your attendance. + +# Best regards, +# KAAUH Recruitment Team +# """ + +# # Send email +# send_mail( +# subject, +# message, +# settings.DEFAULT_FROM_EMAIL, +# [candidate.person.email], +# fail_silently=False, +# ) + +# messages.success(request, f"Invitation email sent to {candidate.person.email}") + +# except Exception as e: +# messages.error(request, f"Failed to send invitation email: {str(e)}") + +# return redirect('meeting_details', slug=slug) + + +# @login_required +# def send_participants_invitation(request, slug): +# """Send invitation email to all participants""" +# meeting = get_object_or_404(InterviewLocation, slug=slug) + +# try: +# interview = meeting.scheduled_interview +# except ScheduledInterview.DoesNotExist: +# raise Http404("No interview is associated with this meeting.") + +# candidate = interview.application +# job = interview.job + +# if request.method == 'POST': +# try: +# from django.core.mail import send_mail +# from django.conf import settings + +# # Get all participants +# participants = list(interview.participants.all()) +# system_users = list(interview.system_users.all()) +# all_participants = participants + system_users + +# if not all_participants: +# messages.warning(request, "No participants found to send invitation to.") +# return redirect('meeting_details', slug=slug) + +# # Simple email content +# subject = f"Interview Invitation - {job.title} with {candidate.person.first_name} {candidate.person.last_name}" +# message = f""" +# Dear Team Member, + +# You are invited to participate in an interview session. + +# Interview Details: +# - Candidate: {candidate.person.first_name} {candidate.person.last_name} +# - Position: {job.title} +# - Date: {interview.interview_date} +# - Time: {interview.interview_time} +# - Duration: {meeting.duration or 60} minutes +# """ + +# # Add join URL if it's a Zoom meeting +# if hasattr(meeting, 'zoommeetingdetails') and meeting.zoommeetingdetails.join_url: +# message += f"- Join URL: {meeting.zoommeetingdetails.join_url}\n" + +# # Add physical address if it's an onsite meeting +# if hasattr(meeting, 'onsitelocationdetails') and meeting.onsitelocationdetails.physical_address: +# message += f"- Location: {meeting.onsitelocationdetails.physical_address}\n" +# if meeting.onsitelocationdetails.room_number: +# message += f"- Room: {meeting.onsitelocationdetails.room_number}\n" + +# message += """ +# Please confirm your availability. + +# Best regards, +# KAAUH Recruitment Team +# """ + +# # Get email addresses of all participants +# recipient_emails = [] +# for participant in all_participants: +# if hasattr(participant, 'email') and participant.email: +# recipient_emails.append(participant.email) + +# if recipient_emails: +# # Send email to all participants +# send_mail( +# subject, +# message, +# settings.DEFAULT_FROM_EMAIL, +# recipient_emails, +# fail_silently=False, +# ) + +# messages.success(request, f"Invitation emails sent to {len(recipient_emails)} participants") +# else: +# messages.warning(request, "No valid email addresses found for participants.") + +# except Exception as e: +# messages.error(request, f"Failed to send invitation emails: {str(e)}") + +# return redirect('meeting_details', slug=slug) diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index d833e49..fc15046 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -7,7 +7,6 @@ from django.http import JsonResponse, HttpResponse from django.db.models.fields.json import KeyTextTransform,KeyTransform from recruitment.utils import json_to_markdown_table from django.db.models import Count, Avg, F, FloatField -from django.db.models.functions import Cast from django.db.models.functions import Coalesce, Cast, Replace, NullIf from . import models from django.utils.translation import get_language @@ -1065,47 +1064,47 @@ def sync_history(request, job_slug=None): #participants views -class ParticipantsListView(LoginRequiredMixin, StaffRequiredMixin, ListView): - model = models.Participants - template_name = 'participants/participants_list.html' - context_object_name = 'participants' - paginate_by = 10 +# class ParticipantsListView(LoginRequiredMixin, StaffRequiredMixin, ListView): +# model = models.Participants +# template_name = 'participants/participants_list.html' +# context_object_name = 'participants' +# paginate_by = 10 - def get_queryset(self): - queryset = super().get_queryset() +# def get_queryset(self): +# queryset = super().get_queryset() - # Handle search - search_query = self.request.GET.get('search', '') - if search_query: - queryset = queryset.filter( - Q(name__icontains=search_query) | - Q(email__icontains=search_query) | - Q(phone__icontains=search_query) | - Q(designation__icontains=search_query) - ) +# # Handle search +# search_query = self.request.GET.get('search', '') +# if search_query: +# queryset = queryset.filter( +# Q(name__icontains=search_query) | +# Q(email__icontains=search_query) | +# Q(phone__icontains=search_query) | +# Q(designation__icontains=search_query) +# ) - # Filter for non-staff users - if not self.request.user.is_staff: - return models.Participants.objects.none() # Restrict for non-staff +# # Filter for non-staff users +# if not self.request.user.is_staff: +# return models.Participants.objects.none() # Restrict for non-staff - return queryset.order_by('-created_at') +# return queryset.order_by('-created_at') - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['search_query'] = self.request.GET.get('search', '') - return context -class ParticipantsDetailView(LoginRequiredMixin, StaffRequiredMixin, DetailView): - model = models.Participants - template_name = 'participants/participants_detail.html' - context_object_name = 'participant' - slug_url_kwarg = 'slug' +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# context['search_query'] = self.request.GET.get('search', '') +# return context +# class ParticipantsDetailView(LoginRequiredMixin, StaffRequiredMixin, DetailView): +# model = models.Participants +# template_name = 'participants/participants_detail.html' +# context_object_name = 'participant' +# slug_url_kwarg = 'slug' -class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView): - model = models.Participants - form_class = forms.ParticipantsForm - template_name = 'participants/participants_create.html' - success_url = reverse_lazy('job_list') - success_message = 'Participant created successfully.' +# class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView): +# model = models.Participants +# form_class = forms.ParticipantsForm +# template_name = 'participants/participants_create.html' +# success_url = reverse_lazy('job_list') +# success_message = 'Participant created successfully.' # def get_initial(self): # initial = super().get_initial() @@ -1116,17 +1115,17 @@ class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMess -class ParticipantsUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView): - model = models.Participants - form_class = forms.ParticipantsForm - template_name = 'participants/participants_create.html' - success_url = reverse_lazy('job_list') - success_message = 'Participant updated successfully.' - slug_url_kwarg = 'slug' +# class ParticipantsUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView): +# model = models.Participants +# form_class = forms.ParticipantsForm +# template_name = 'participants/participants_create.html' +# success_url = reverse_lazy('job_list') +# success_message = 'Participant updated successfully.' +# slug_url_kwarg = 'slug' -class ParticipantsDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView): - model = models.Participants +# class ParticipantsDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView): +# model = models.Participants - success_url = reverse_lazy('participants_list') # Redirect to the participants list after success - success_message = 'Participant deleted successfully.' - slug_url_kwarg = 'slug' +# success_url = reverse_lazy('participants_list') # Redirect to the participants list after success +# success_message = 'Participant deleted successfully.' +# slug_url_kwarg = 'slug' diff --git a/templates/base.html b/templates/base.html index 438826f..b2c6b69 100644 --- a/templates/base.html +++ b/templates/base.html @@ -274,7 +274,7 @@