diff --git a/.env b/.env index 8d7fbd5..b9e2bf0 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -DB_NAME=haikal_db -DB_USER=faheed -DB_PASSWORD=Faheed@215 \ No newline at end of file +DB_NAME=norahuniversity +DB_USER=norahuniversity +DB_PASSWORD=norahuniversity \ No newline at end of file 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 deleted file mode 100644 index db362f9..0000000 --- a/recruitment/migrations/0001_initial.py +++ /dev/null @@ -1,768 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-17 09:52 - -import django.contrib.auth.models -import django.contrib.auth.validators -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -import django_ckeditor_5.fields -import django_countries.fields -import django_extensions.db.fields -import recruitment.validators -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='BreakTime', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start_time', models.TimeField(verbose_name='Start Time')), - ('end_time', models.TimeField(verbose_name='End Time')), - ], - ), - migrations.CreateModel( - name='FormStage', - 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')), - ('name', models.CharField(help_text='Name of the stage', max_length=200)), - ('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')), - ('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')), - ], - options={ - 'verbose_name': 'Form Stage', - 'verbose_name_plural': 'Form Stages', - 'ordering': ['order'], - }, - ), - migrations.CreateModel( - name='InterviewLocation', - 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')), - ('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')), - ], - options={ - 'verbose_name': 'Interview Location', - 'verbose_name_plural': 'Interview Locations', - }, - ), - migrations.CreateModel( - name='Participants', - 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')), - ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), - ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), - ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Source', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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')), - ('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')), - ('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')), - ('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')), - ('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')), - ('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')), - ('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')), - ('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')), - ('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')), - ('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')), - ('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')), - ('sync_endpoint', models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint')), - ('sync_method', models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method')), - ('test_method', models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method')), - ('custom_headers', models.TextField(blank=True, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers')), - ('supports_outbound_sync', models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync')), - ], - options={ - 'verbose_name': 'Source', - 'verbose_name_plural': 'Sources', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='CustomUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('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')), - ('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')), - ('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')), - ('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')), - ], - options={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='FormField', - 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')), - ('label', models.CharField(help_text='Label for the field', max_length=200)), - ('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)), - ('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)), - ('required', models.BooleanField(default=False, help_text='Whether the field is required')), - ('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')), - ('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')), - ('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')), - ('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)), - ('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')), - ('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')), - ('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')), - ('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')), - ], - options={ - 'verbose_name': 'Form Field', - 'verbose_name_plural': 'Form Fields', - 'ordering': ['order'], - }, - ), - migrations.CreateModel( - name='FormTemplate', - 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')), - ('name', models.CharField(help_text='Name of the form template', max_length=200)), - ('description', models.TextField(blank=True, help_text='Description of the form template')), - ('is_active', models.BooleanField(default=False, help_text='Whether this template is active')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Form Template', - 'verbose_name_plural': 'Form Templates', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='FormSubmission', - 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')), - ('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)), - ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), - ('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)), - ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)), - ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')), - ], - options={ - 'verbose_name': 'Form Submission', - 'verbose_name_plural': 'Form Submissions', - 'ordering': ['-submitted_at'], - }, - ), - migrations.AddField( - model_name='formstage', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'), - ), - migrations.CreateModel( - name='HiringAgency', - 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')), - ('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')), - ('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')), - ('email', models.EmailField(blank=True, max_length=254)), - ('phone', models.CharField(blank=True, max_length=20)), - ('website', models.URLField(blank=True)), - ('notes', models.TextField(blank=True, help_text='Internal notes about the agency')), - ('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)), - ('address', models.TextField(blank=True, null=True)), - ('generated_password', models.CharField(blank=True, help_text='Generated password for agency user account', max_length=255, null=True)), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agency_profile', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'Hiring Agency', - 'verbose_name_plural': 'Hiring Agencies', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Application', - 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')), - ('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')), - ('cover_letter', models.FileField(blank=True, null=True, upload_to='cover_letters/', verbose_name='Cover Letter')), - ('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')), - ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), - ('applied', models.BooleanField(default=False, verbose_name='Applied')), - ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Document Review', 'Document Review'), ('Offer', 'Offer'), ('Hired', 'Hired'), ('Rejected', 'Rejected')], db_index=True, default='Applied', max_length=20, verbose_name='Stage')), - ('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=20, null=True, verbose_name='Applicant Status')), - ('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')), - ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Exam Status')), - ('exam_score', models.FloatField(blank=True, null=True, verbose_name='Exam Score')), - ('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')), - ('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Interview Status')), - ('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')), - ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected'), ('Pending', 'Pending')], max_length=20, null=True, verbose_name='Offer Status')), - ('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')), - ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), - ('ai_analysis_data', models.JSONField(blank=True, default=dict, help_text='Full JSON output from the resume scoring model.', null=True, verbose_name='AI Analysis Data')), - ('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')), - ('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')), - ('hiring_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='recruitment.hiringagency', verbose_name='Hiring Agency')), - ], - options={ - 'verbose_name': 'Application', - 'verbose_name_plural': 'Applications', - }, - ), - migrations.CreateModel( - name='OnsiteLocationDetails', - 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)), - ], - options={ - 'verbose_name': 'Onsite Location Details', - 'verbose_name_plural': 'Onsite Location Details', - }, - 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', - 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')), - ('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)), - ('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)), - ('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')), - ('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), - ('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)), - ('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), - ('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), - ('application_deadline', models.DateField(db_index=True)), - ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), - ('internal_job_id', models.CharField(editable=False, max_length=50)), - ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), - ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)), - ('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])), - ('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)), - ('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')), - ('posted_to_linkedin', models.BooleanField(default=False)), - ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), - ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), - ('linkedin_post_formated_data', models.TextField(blank=True, null=True)), - ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), - ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), - ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), - ('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')), - ('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)), - ('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')), - ('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')), - ('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')), - ], - options={ - 'verbose_name': 'Job Posting', - 'verbose_name_plural': 'Job Postings', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='InterviewSchedule', - 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')), - ('start_time', models.TimeField(verbose_name='Start Time')), - ('end_time', models.TimeField(verbose_name='End Time')), - ('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')), - ('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')), - ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), - ('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)')), - ('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', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.jobposting', verbose_name='Job'), - ), - migrations.CreateModel( - name='AgencyJobAssignment', - 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')), - ('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')), - ('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')), - ('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')), - ('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')), - ('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')), - ('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')), - ('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')), - ('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')), - ], - options={ - 'verbose_name': 'Agency Job Assignment', - 'verbose_name_plural': 'Agency Job Assignments', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='JobPostingImage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('post_image', models.ImageField(upload_to='post/', validators=[recruitment.validators.validate_image_size])), - ('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')), - ], - ), - migrations.CreateModel( - name='Message', - 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')), - ('subject', models.CharField(max_length=200, verbose_name='Subject')), - ('content', models.TextField(verbose_name='Message Content')), - ('message_type', models.CharField(choices=[('direct', 'Direct Message'), ('job_related', 'Job Related'), ('system', 'System Notification')], default='direct', max_length=20, verbose_name='Message Type')), - ('is_read', models.BooleanField(default=False, verbose_name='Is Read')), - ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.jobposting', verbose_name='Related Job')), - ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), - ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), - ], - options={ - 'verbose_name': 'Message', - 'verbose_name_plural': 'Messages', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='Notification', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message', models.TextField(verbose_name='Notification Message')), - ('notification_type', models.CharField(choices=[('email', 'Email'), ('in_app', 'In-App')], default='email', max_length=20, verbose_name='Notification Type')), - ('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('read', 'Read'), ('failed', 'Failed'), ('retrying', 'Retrying')], default='pending', max_length=20, verbose_name='Status')), - ('scheduled_for', models.DateTimeField(help_text='The date and time this notification is scheduled to be sent.', verbose_name='Scheduled Send Time')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('attempts', models.PositiveIntegerField(default=0, verbose_name='Send Attempts')), - ('last_error', models.TextField(blank=True, verbose_name='Last Error Message')), - ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), - ], - options={ - 'verbose_name': 'Notification', - 'verbose_name_plural': 'Notifications', - 'ordering': ['-scheduled_for', '-created_at'], - }, - ), - migrations.CreateModel( - name='Person', - 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')), - ('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')), - ('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')), - ('gpa', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='GPA')), - ('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')), - ('address', models.TextField(blank=True, null=True, verbose_name='Address')), - ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), - ('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')), - ('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')), - ], - options={ - 'verbose_name': 'Person', - 'verbose_name_plural': 'People', - }, - ), - migrations.AddField( - model_name='application', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.person', verbose_name='Person'), - ), - migrations.CreateModel( - name='ScheduledInterview', - 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')), - ('interview_date', models.DateField(db_index=True, verbose_name='Interview Date')), - ('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')), - ('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')), - ('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=[ - ('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')), - ('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')), - ('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)), - ('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')), - ], - options={ - 'verbose_name': 'Shared Form Template', - 'verbose_name_plural': 'Shared Form Templates', - }, - ), - migrations.CreateModel( - name='IntegrationLog', - 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')), - ('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')), - ('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')), - ('method', models.CharField(blank=True, max_length=50, verbose_name='HTTP Method')), - ('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')), - ('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')), - ('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')), - ('error_message', models.TextField(blank=True, verbose_name='Error Message')), - ('ip_address', models.GenericIPAddressField(verbose_name='IP Address')), - ('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')), - ('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')), - ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')), - ], - options={ - 'verbose_name': 'Integration Log', - 'verbose_name_plural': 'Integration Logs', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='TrainingMaterial', - 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')), - ('title', models.CharField(max_length=255, verbose_name='Title')), - ('content', django_ckeditor_5.fields.CKEditor5Field(blank=True, verbose_name='Content')), - ('video_link', models.URLField(blank=True, verbose_name='Video Link')), - ('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')), - ], - options={ - 'verbose_name': 'Training Material', - 'verbose_name_plural': 'Training Materials', - }, - ), - migrations.CreateModel( - name='AgencyAccessLink', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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')), - ('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')), - ('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')), - ('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')), - ('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')), - ], - options={ - 'verbose_name': 'Agency Access Link', - 'verbose_name_plural': 'Agency Access Links', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx')], - }, - ), - migrations.CreateModel( - name='Document', - 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')), - ('object_id', models.PositiveIntegerField(verbose_name='Object ID')), - ('file', models.FileField(upload_to='documents/%Y/%m/', validators=[recruitment.validators.validate_image_size], verbose_name='Document File')), - ('document_type', models.CharField(choices=[('resume', 'Resume'), ('cover_letter', 'Cover Letter'), ('certificate', 'Certificate'), ('id_document', 'ID Document'), ('passport', 'Passport'), ('education', 'Education Document'), ('experience', 'Experience Letter'), ('other', 'Other')], default='other', max_length=20, verbose_name='Document Type')), - ('description', models.CharField(blank=True, max_length=200, verbose_name='Description')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content Type')), - ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Uploaded By')), - ], - options={ - 'verbose_name': 'Document', - 'verbose_name_plural': 'Documents', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['content_type', 'object_id', 'document_type', 'created_at'], name='recruitment_content_547650_idx')], - }, - ), - migrations.CreateModel( - name='FieldResponse', - 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')), - ('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)), - ('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')), - ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), - ], - options={ - 'verbose_name': 'Field Response', - 'verbose_name_plural': 'Field Responses', - 'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')], - }, - ), - migrations.AddIndex( - 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'), - ), - migrations.AddIndex( - model_name='formtemplate', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'), - ), - migrations.AlterUniqueTogether( - name='agencyjobassignment', - unique_together={('agency', 'job')}, - ), - migrations.AddIndex( - model_name='message', - index=models.Index(fields=['sender', 'created_at'], name='recruitment_sender__49d984_idx'), - ), - migrations.AddIndex( - model_name='message', - index=models.Index(fields=['recipient', 'is_read', 'created_at'], name='recruitment_recipie_af0e6d_idx'), - ), - migrations.AddIndex( - model_name='message', - index=models.Index(fields=['job', 'created_at'], name='recruitment_job_id_18f813_idx'), - ), - migrations.AddIndex( - model_name='message', - index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'), - ), - migrations.AddIndex( - model_name='person', - index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'), - ), - migrations.AddIndex( - model_name='person', - index=models.Index(fields=['first_name', 'last_name'], name='recruitment_first_n_739de5_idx'), - ), - migrations.AddIndex( - model_name='person', - index=models.Index(fields=['created_at'], name='recruitment_created_33495a_idx'), - ), - migrations.AddIndex( - model_name='application', - index=models.Index(fields=['person', 'job'], name='recruitment_person__34355c_idx'), - ), - migrations.AddIndex( - model_name='application', - index=models.Index(fields=['stage'], name='recruitment_stage_52c2d1_idx'), - ), - migrations.AddIndex( - model_name='application', - index=models.Index(fields=['created_at'], name='recruitment_created_80633f_idx'), - ), - migrations.AddIndex( - model_name='application', - index=models.Index(fields=['person', 'stage', 'created_at'], name='recruitment_person__8715ec_idx'), - ), - migrations.AlterUniqueTogether( - name='application', - unique_together={('person', 'job')}, - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['application', 'job'], name='recruitment_applica_927561_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), - ), - migrations.AddIndex( - 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/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/migrations/0007_alter_person_email.py b/recruitment/migrations/0007_alter_person_email.py deleted file mode 100644 index 7390323..0000000 --- a/recruitment/migrations/0007_alter_person_email.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 12:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_alter_customuser_email'), - ] - - operations = [ - migrations.AlterField( - model_name='person', - name='email', - field=models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email'), - ), - ] diff --git a/recruitment/migrations/__init__.py b/recruitment/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recruitment/models.py b/recruitment/models.py index 1441182..a8975a2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -295,10 +295,10 @@ class JobPosting(Base): next_num = 1 self.internal_job_id = f"{prefix}-{year}-{next_num:06d}" - + if self.department: self.department = self.department.title() - + super().save(*args, **kwargs) def get_location_display(self): @@ -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/requirements.txt b/requirements.txt index 5fee9fe..36e09c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,146 +1,191 @@ -annotated-types -appdirs -asgiref -asteval -astunparse -attrs -blinker -blis -boto3 -botocore -bw-migrations -bw2parameters -bw_processing -cached-property -catalogue -certifi -channels -chardet -charset-normalizer -click -cloudpathlib -confection -constructive_geometries -country_converter -cymem -dataflows-tabulator -datapackage -deepdiff -Deprecated -Django -django-allauth -django-cors-headers -django-filter -django-unfold -djangorestframework -docopt +amqp==5.3.1 +annotated-types==0.7.0 +anthropic==0.63.0 +anyio==4.11.0 +appdirs==1.4.4 +arrow==1.3.0 +asgiref==3.9.2 +asteval==1.0.6 +astunparse==1.6.3 +attrs==25.3.0 +billiard==4.2.2 +bleach==6.2.0 +blessed==1.22.0 +blinker==1.9.0 +blis==1.3.0 +boto3==1.40.37 +botocore==1.40.37 +bw-migrations==0.2 +bw2data==4.5 +bw2parameters==1.1.0 +bw_processing==1.0 +cached-property==2.0.1 +catalogue==2.0.10 +celery==5.5.3 +certifi==2025.8.3 +channels==4.3.1 +chardet==5.2.0 +charset-normalizer==3.4.3 +click==8.3.0 +click-didyoumean==0.3.1 +click-plugins==1.1.1.2 +click-repl==0.3.0 +cloudpathlib==0.22.0 +confection==0.1.5 +constructive_geometries==1.0 +country_converter==1.3.1 +crispy-bootstrap5==2025.6 +cymem==2.0.11 +dataflows-tabulator==1.54.3 +datapackage==1.15.4 +datastar-py==0.6.5 +deepdiff==7.0.1 +Deprecated==1.2.18 +distro==1.9.0 +Django==5.2.6 +django-allauth==65.11.2 +django-ckeditor-5==0.2.18 +django-cors-headers==4.9.0 +django-countries==7.6.1 +django-crispy-forms==2.4 +django-easy-audit==1.3.7 +django-extensions==4.1 +django-filter==25.1 +django-picklefield==3.3 +django-q2==1.8.0 +django-summernote==0.8.20.0 +django-template-partials==25.2 +django-unfold==0.66.0 +django-widget-tweaks==1.5.0 +django_celery_results==2.6.0 +djangorestframework==3.16.1 +docopt==0.6.2 en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85 -et_xmlfile -Faker -flexcache -flexparser -fsspec -idna -ijson -isodate -Jinja2 -jmespath -jsonlines -jsonpointer -jsonschema -jsonschema-specifications -langcodes -language_data -linear-tsv -llvmlite -loguru -lxml -marisa-trie -markdown-it-py -MarkupSafe -matrix_utils -mdurl -morefs -mrio-common-metadata -murmurhash -numba -numpy -openpyxl -ordered-set -packaging -pandas -peewee -Pint -platformdirs -preshed -prettytable -pydantic -pydantic-settings -pydantic_core -pyecospold -Pygments -PyJWT -PyMuPDF -pyparsing -PyPrind -python-dateutil -python-dotenv -python-json-logger -pytz -pyxlsb -PyYAML -randonneur -randonneur_data -RapidFuzz -rdflib -referencing -requests -rfc3986 -rich -rpds-py -s3transfer -scipy -shellingham -six -smart-open -snowflake-id -spacy -spacy-legacy -spacy-loggers -SPARQLWrapper -sparse -SQLAlchemy -sqlparse -srsly -stats_arrays -structlog -tableschema -thinc -toolz -tqdm -typer -typing-inspection -typing_extensions -tzdata -unicodecsv -urllib3 -voluptuous -wasabi -wcwidth -weasel -wrapt -wurst -xlrd -XlsxWriter -celery[redis] -redis -sentence-transformers -torch -pdfplumber -python-docx -PyMuPDF -pytesseract -Pillow -python-dotenv -django-countries -django-q2 \ No newline at end of file +et_xmlfile==2.0.0 +Faker==37.8.0 +flexcache==0.3 +flexparser==0.4 +fsspec==2025.9.0 +gpt-po-translator==1.3.2 +greenlet==3.2.4 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.10 +ijson==3.4.0 +iniconfig==2.1.0 +isodate==0.7.2 +isort==5.13.2 +Jinja2==3.1.6 +jiter==0.11.1 +jmespath==1.0.1 +jsonlines==4.0.0 +jsonpointer==3.0.0 +jsonschema==4.25.1 +jsonschema-specifications==2025.9.1 +kombu==5.5.4 +langcodes==3.5.0 +language_data==1.3.0 +linear-tsv==1.1.0 +llvmlite==0.45.0 +loguru==0.7.3 +lxml==6.0.2 +marisa-trie==1.3.1 +markdown-it-py==4.0.0 +MarkupSafe==3.0.2 +matrix_utils==0.6.2 +mdurl==0.1.2 +morefs==0.2.2 +mrio-common-metadata==0.2.1 +murmurhash==1.0.13 +numba==0.62.0 +numpy==2.3.3 +openai==1.99.9 +openpyxl==3.1.5 +ordered-set==4.1.0 +packaging==25.0 +pandas==2.3.2 +peewee==3.18.2 +pillow==11.3.0 +Pint==0.25 +platformdirs==4.4.0 +pluggy==1.6.0 +polib==1.2.0 +preshed==3.0.10 +prettytable==3.16.0 +prompt_toolkit==3.0.52 +psycopg2-binary==2.9.11 +pycountry==24.6.1 +pydantic==2.11.9 +pydantic-settings==2.10.1 +pydantic_core==2.33.2 +pyecospold==4.0.0 +Pygments==2.19.2 +PyJWT==2.10.1 +PyMuPDF==1.26.4 +pyparsing==3.2.5 +PyPDF2==3.0.1 +PyPrind==2.11.3 +pytest==8.3.4 +pytest-django==4.11.1 +python-dateutil==2.9.0.post0 +python-docx==1.2.0 +python-dotenv==1.0.1 +python-json-logger==3.3.0 +pytz==2025.2 +pyxlsb==1.0.10 +PyYAML==6.0.2 +randonneur==0.6.2 +randonneur_data==0.6 +RapidFuzz==3.14.1 +rdflib==7.2.1 +redis==3.5.3 +referencing==0.36.2 +requests==2.32.3 +responses==0.25.8 +rfc3986==2.0.0 +rich==14.1.0 +rpds-py==0.27.1 +s3transfer==0.14.0 +scipy==1.16.2 +setuptools==80.9.0 +setuptools-scm==8.1.0 +shellingham==1.5.4 +six==1.17.0 +smart_open==7.3.1 +sniffio==1.3.1 +snowflake-id==1.0.2 +spacy==3.8.7 +spacy-legacy==3.0.12 +spacy-loggers==1.0.5 +SPARQLWrapper==2.0.0 +sparse==0.17.0 +SQLAlchemy==2.0.43 +sqlparse==0.5.3 +srsly==2.5.1 +stats_arrays==0.7 +structlog==25.4.0 +tableschema==1.21.0 +tenacity==9.0.0 +thinc==8.3.6 +tomli==2.2.1 +toolz==1.0.0 +tqdm==4.67.1 +typer==0.19.2 +types-python-dateutil==2.9.0.20251008 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +tzdata==2025.2 +unicodecsv==0.14.1 +urllib3==2.5.0 +vine==5.1.0 +voluptuous==0.15.2 +wasabi==1.1.3 +wcwidth==0.2.14 +weasel==0.4.1 +webencodings==0.5.1 +wheel==0.45.1 +wrapt==1.17.3 +wurst==0.4 +xlrd==2.0.2 +xlsxwriter==3.2.9 diff --git a/templates/account/login.html b/templates/account/login.html index 5cf52e8..ba7a332 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -7,10 +7,10 @@ KAAUH ATS - Sign In (Bootstrap) - + - - + + +{% endblock %} + +{% block content %} +
+ +
+
+

+ + {% trans "Interview Details" %} +

+

+ {{ interview.application.name }} - {{ interview.job.title }} +

+
+ +
+ +
+ +
+ +
+
+
+ {% trans "Candidate Information" %} +
+
+ {% if interview.application.resume %} + + {% trans "Download Resume" %} + + {% endif %} + +
+
+ +
+
+
+
{% trans "Personal Details" %}
+

{% trans "Name:" %} {{ interview.application.name }}

+

{% trans "Email:" %} {{ interview.application.email }}

+

{% trans "Phone:" %} {{ interview.application.phone }}

+ {% if interview.application.location %} +

{% trans "Location:" %} {{ interview.application.location }}

+ {% endif %} +
+
+
+
+
{% trans "Application Details" %}
+

{% trans "Job:" %} {{ interview.job.title }}

+

{% trans "Department:" %} {{ interview.job.department }}

+

{% trans "Applied Date:" %} {{ interview.application.created_at|date:"d-m-Y" }}

+

{% trans "Current Stage:" %} + + {{ interview.application.stage }} + +

+
+
+
+
+ + +
+
+
+ {% trans "Interview Details" %} +
+
+ + {% if interview.interview.location_type == 'Remote' %} + {% trans "Remote" %} + {% else %} + {% trans "Onsite" %} + {% endif %} + + + {{ interview.status }} + +
+
+ +
+
+
+
+ {% trans "Date:" %} + {{ interview.interview_date|date:"d-m-Y" }} +
+
+ {% trans "Time:" %} + {{ interview.interview_time|date:"h:i A" }} +
+
+ {% trans "Duration:" %} + {{ interview.interview.duration }} {% trans "minutes" %} +
+
+
+
+ {% if interview.interview.location_type == 'Remote' %} +
+
{% trans "Remote Meeting Details" %}
+
+ {% trans "Platform:" %} + Zoom +
+ {% if interview.interview %} +
+ {% trans "Meeting ID:" %} + {{ interview.interview.meeting_id }} +
+
+ {% trans "Password:" %} + {{ interview.interview.password }} +
+ {% if interview.interview.details_url %} + + {% endif %} + {% endif %} +
+ {% else %} +
+
{% trans "Onsite Location Details" %}
+ {% if interview.interview %} +
+ {% trans "Address:" %} + {{ interview.interview.physical_address }} +
+
+ {% trans "Room:" %} + {{ interview.interview.room_number }} +
+ {% endif %} +
+ {% endif %} +
+
+
+ + +
+
+ {% trans "Interview Timeline" %} +
+
+
+
+
+
+
{% trans "Interview Scheduled" %}
+

{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"d-m-Y" }} {{ interview.interview_time|date:"h:i A" }}

+
+ {{ interview.interview.created_at|date:"d-m-Y h:i A" }} +
+
+
+ {% if interview.interview.status == 'CONFIRMED' %} +
+
+
+
+
{% trans "Interview Confirmed" %}
+

{% trans "Candidate has confirmed attendance" %}

+
+ {% trans "Recently" %} +
+
+
+ {% endif %} + {% if interview.interview.status == 'COMPLETED' %} +
+
+
+
+
{% trans "Interview Completed" %}
+

{% trans "Interview has been completed" %}

+
+ {% trans "Recently" %} +
+
+
+ {% endif %} + {% if interview.interview.status == 'CANCELLED' %} +
+
+
+
+
{% trans "Interview Cancelled" %}
+

{% trans "Interview was cancelled" %}

+
+ {% trans "Recently" %} +
+
+
+ {% endif %} +
+
+
+ + +
+ +
+
+ {% trans "Participants" %} +
+ + + {% if interview.participants.exists %} +
{% trans "Internal Participants" %}
+ {% for participant in interview.participants.all %} +
+
+ {{ participant.first_name.0 }}{{ participant.last_name.0 }} +
+
+
{{ participant.get_full_name }}
+
{{ participant.email }}
+
+
+ {% endfor %} + {% endif %} + + + {% if interview.system_users.exists %} +
{% trans "External Participants" %}
+ {% for user in interview.system_users.all %} +
+
+ {{ user.first_name.0 }}{{ user.last_name.0 }} +
+
+
{{ user.get_full_name }}
+
{{ user.email }}
+
+
+ {% endfor %} + {% endif %} + + {% if not interview.participants.exists and not interview.system_users.exists %} +
+ +

{% trans "No participants added yet" %}

+
+ {% endif %} + + +
+ + +
+
+ {% trans "Actions" %} +
+ +
+ {% if interview.status != 'CANCELLED' and interview.status != 'COMPLETED' %} + + + + {% endif %} + + + + {% if interview.status == 'COMPLETED' %} + + {% endif %} +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +{% endblock %} + +{% block customJS %} + +{% endblock %} diff --git a/templates/interviews/interview_list.html b/templates/interviews/interview_list.html index 7a7fd6e..e329ffa 100644 --- a/templates/interviews/interview_list.html +++ b/templates/interviews/interview_list.html @@ -1,80 +1,234 @@ -{% extends "base.html" %} +{% extends 'base.html' %} {% load static i18n %} -{% block title %}{% trans "Scheduled Interviews List" %} - {{ block.super }}{% endblock %} +{% block title %}{% trans "Interview Management" %} - ATS{% endblock %} {% block customCSS %} -{# (Your existing CSS is kept here, as it is perfect for the theme) #} {% endblock %} {% block content %} -{{interviews}}
+
-

- {% trans "Scheduled Interviews" %} -

- {# FIX: Using safe anchor href="#" to prevent the NoReverseMatch crash. #} - {# Replace '#' with {% url 'create_scheduled_interview' %} once the URL name is defined in urls.py #} - - {% trans "Schedule Interview" %} - +
+

+ + {% trans "Interview Management" %} +

+

+ {% trans "Total Interviews:" %} {{ interviews|length }} +

+
+
-
-
-
- {# Search field #} -
- -
- -
-
- - {# Filter by Status #} -
- - -
- - {# Filter by Interview Type (ONSITE/REMOTE) - This list now correctly populated #} -
- - -
- -
-
- - {% if status_filter or search_query or type_filter %} - {# Assuming 'interview_list' is the URL name for this view #} - - {% trans "Clear" %} - - {% endif %} -
-
-
-
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + {% trans "Clear" %} + +
+
{{meetings}} {# Using 'meetings' based on the context_object_name provided #} @@ -99,11 +253,11 @@

- {% trans "Job" %}: + {% trans "Job" %}: {{ interview.job.title }}
- + {# --- Remote/Onsite Logic - Handles both cases safely --- #} - + {% trans "Type" %}: {{ interview.schedule.get_interview_type_display }} {% if interview.schedule.interview_type == 'Remote' %}
{# CRITICAL FIX: Safe access to zoom_meeting details #} @@ -111,7 +265,7 @@ {% else %}
{% trans "Location" %}: {{ interview.schedule.location }} {% endif %}
- + {% trans "Date" %}: {{ interview.interview_date|date:"M d, Y" }}
{% trans "Time" %}: {{ interview.interview_time|time:"H:i" }}
{% trans "Duration" %}: {{ interview.schedule.interview_duration }} minutes @@ -149,70 +303,83 @@ {% endfor %}

- {# Table View (Logic is identical, safe access applied) #} -
+
+ {% csrf_token %}
- +
- - - - - - - + + + + + + {% comment %} {% endcomment %} + - {% for interview in meetings %} + {% for interview in interviews %} - - + - @@ -220,49 +387,138 @@
{% trans "Candidate" %}{% trans "Job" %}{% trans "Type" %}{% trans "Date/Time" %}{% trans "Duration" %}{% trans "Status" %}{% trans "Actions" %} {% trans "Candidate" %} {% trans "Job" %} {% trans "Date & Time" %} {% trans "Type" %} {% trans "Status" %} {% trans "Participants" %} {% trans "Actions" %}
- - {{ interview.candidate.name }} - +
{{ interview.application.name }}
+
+ {{ interview.application.email }}
+ {{ interview.application.phone }} +
- {{ interview.job.title }} +
{{ interview.job.title }}
+
{{ interview.job.department }}
- {{ interview.schedule.get_interview_type_display }} +
+ {{ interview.interview_date|date:"d-m-Y" }}
+ {{ interview.interview_time|date:"h:i A" }} +
{{ interview.interview_date|date:"M d, Y" }}
({{ interview.interview_time|time:"H:i" }})
{{ interview.schedule.interview_duration }} min - - {% if interview.status == 'confirmed' %} - - {% endif %} - {{ interview.status|title }} + {% if interview.interview.location_type == 'Remote' %} + + {% trans "Remote" %} + + {% else %} + + {% trans "Onsite" %} + + {% endif %} + + + {{ interview.status|upper }} -
- - {# CRITICAL FIX: Safe access to join URL #} - {% if interview.schedule.interview_type == 'Remote' and interview.zoom_meeting and interview.zoom_meeting.join_url %} - - - - {% endif %} - +
+
+ - - - - + {% comment %} {% if interview.status != 'CANCELLED' and interview.status != 'COMPLETED' %} + + + {% endif %} {% endcomment %}
+
+ + + {% if is_paginated %} + + {% endif %} + + {% else %} + -
- - {# Pagination #} - {% if is_paginated %} - {% endif %} - {% else %} -
-
- -

{% trans "No Interviews found" %}

-

{% trans "Schedule your first interview or adjust your filters." %}

- {# FIX: Using safe anchor href="#" to prevent the NoReverseMatch crash. #} - - {% trans "Schedule an Interview" %} - +
+
+ + + -{% endblock %} \ No newline at end of file + +{% endblock %} + +{% block customJS %} + +{% endblock %} diff --git a/templates/interviews/partials/interview_list.html b/templates/interviews/partials/interview_list.html new file mode 100644 index 0000000..04cc425 --- /dev/null +++ b/templates/interviews/partials/interview_list.html @@ -0,0 +1,42 @@ +{% load i18n %} + + + + + + + + + + + + + {% for interview in interviews %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Topic" %} {% trans "Date" %} {% trans "Duration" %} {% trans "Location" %} {% trans "Status" %} {% trans "Actions" %}
{{ interview.interview.topic }}{{ interview.interview_date }} {{interview.interview_time}}{{ interview.interview.duration }} + + {{ interview.interview.location_type }} + + + + {{ interview.get_status_display }} + + View
+ + {% trans "No interviews scheduled yet." %} +
\ No newline at end of file diff --git a/templates/people/delete_person.html b/templates/people/delete_person.html new file mode 100644 index 0000000..0f79a9d --- /dev/null +++ b/templates/people/delete_person.html @@ -0,0 +1,372 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Delete Person" %} - {{ block.super }}{% endblock %} + +{% block customCSS %} + +{% endblock %} + +{% block content %} +
+ +
+
+

+ + {% trans "Delete Person" %} +

+

+ {% trans "You are about to delete a person record. This action cannot be undone." %} +

+
+ + {% trans "Back to Person" %} + +
+ +
+
+ +
+
+ +
+

{% trans "Warning: This action cannot be undone!" %}

+

+ {% trans "Deleting this person will permanently remove all associated data. Please review the information below carefully before proceeding." %} +

+
+ + +
+
+
+ + {% trans "Person to be Deleted" %} +
+
+
+
+
+ {% if object.profile_image %} + {{ object.get_full_name }} + {% else %} +
+ +
+ {% endif %} +
+

{{ object.get_full_name }}

+ {% if object.email %} +

{{ object.email }}

+ {% endif %} +
+
+ + {% if object.phone %} +
+
+ +
+
+
{% trans "Phone" %}
+
{{ object.phone }}
+
+
+ {% endif %} + +
+
+ +
+
+
{% trans "Created On" %}
+
{{ object.created_at|date:"F d, Y" }}
+
+
+ + {% if object.nationality %} +
+
+ +
+
+
{% trans "Nationality" %}
+
{{ object.nationality }}
+
+
+ {% endif %} + + {% if object.gender %} +
+
+ +
+
+
{% trans "Gender" %}
+
{{ object.get_gender_display }}
+
+
+ {% endif %} +
+
+
+ + +
+
+
+ + {% trans "What will happen when you delete this person?" %} +
+
+
+
    +
  • + + {% trans "The person profile and all personal information will be permanently deleted" %} +
  • +
  • + + {% trans "All associated applications and documents will be removed" %} +
  • +
  • + + {% trans "Any interview schedules and history will be deleted" %} +
  • +
  • + + {% trans "All related data and records will be lost" %} +
  • +
  • + + {% trans "This action cannot be undone under any circumstances" %} +
  • +
+
+
+ + +
+
+
+ {% csrf_token %} + +
+
+ + +
+
+ +
+ + + {% trans "Cancel" %} + + +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/templates/people/person_confirm_delete.html b/templates/people/person_confirm_delete.html new file mode 100644 index 0000000..e8711d1 --- /dev/null +++ b/templates/people/person_confirm_delete.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Delete Person" %} - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+

+ + {% trans "Confirm Deletion" %} +

+
+
+ + +
+ {% if person.profile_image %} + {{ person.get_full_name }} + {% else %} +
+ +
+ {% endif %} +
{{ person.get_full_name }}
+ {% if person.email %} +

{{ person.email }}

+ {% endif %} +
+ +
+ {% csrf_token %} +
+ + {% trans "Cancel" %} + + +
+
+
+
+
+
+
+{% endblock %} diff --git a/templates/people/person_list.html b/templates/people/person_list.html index af8a475..375169e 100644 --- a/templates/people/person_list.html +++ b/templates/people/person_list.html @@ -163,7 +163,7 @@
- +
@@ -213,8 +213,8 @@
- - + + {% if people_list %}
@@ -287,13 +287,13 @@ class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> - + {% endcomment %} {% endif %}
diff --git a/templates/people/update_person.html b/templates/people/update_person.html index f23d075..33d35ec 100644 --- a/templates/people/update_person.html +++ b/templates/people/update_person.html @@ -194,6 +194,9 @@ {% trans "View Details" %} + + {% trans "Delete" %} + {% trans "Back to List" %} diff --git a/templates/recruitment/agency_form.html b/templates/recruitment/agency_form.html index c03d06b..4f72f50 100644 --- a/templates/recruitment/agency_form.html +++ b/templates/recruitment/agency_form.html @@ -1,217 +1,478 @@ -{% extends 'base.html' %} -{% load static i18n %} +{% extends "base.html" %} +{% load static i18n widget_tweaks %} -{% block title %}{{ title }} - ATS{% endblock %} +{% block title %}{{ title }} - {{ block.super }}{% endblock %} + +{% block customCSS %} + +{% endblock %} {% block content %} -
- -
-
-

{{ title }}

-

+

+
+ + + + + - - {% trans "Back to Agencies" %} - -
- -
-
-
-
- {% if form.non_field_errors %} - +{% endblock %} +{% block customJS %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/recruitment/application_update.html b/templates/recruitment/application_update.html index dd38079..d5af670 100644 --- a/templates/recruitment/application_update.html +++ b/templates/recruitment/application_update.html @@ -1,43 +1,51 @@ {% extends "base.html" %} {% load static i18n crispy_forms_tags %} -{% block title %}Update Candidate - {{ block.super }}{% endblock %} +{% block title %}Update {{ object.name }} - {{ block.super }}{% endblock %} {% block customCSS %} {% endblock %} {% block content %}
- -
-
-
-
-

- - {% trans "Update Candidate:" %} {{ object.name }} -

-

{% trans "Edit candidate information and details" %}

-
-
- - - {% trans "Back to List" %} +
+ + + + + + + +
+
+
+
{% trans "Currently Editing" %}
+
+ {% if object.profile_image %} + {{ object.name }} + {% else %} +
+ +
+ {% endif %} +
+
{{ object.name }}
+ {% if object.email %} +

{{ object.email }}

+ {% endif %} + + {% trans "Created" %}: {{ object.created_at|date:"d M Y" }} • + {% trans "Last Updated" %}: {{ object.updated_at|date:"d M Y" }} + +
+
-
-
-
-

- - {% trans "Candidate Form" %} -

-
-
-
- {% csrf_token %} - - {# Use Crispy Forms to render fields. The two-column layout is applied to the main form content #} -
- {% for field in form %} -
- {{ field|as_crispy_field }} + +
+
+ {% if form.non_field_errors %} + + {% endif %} + + {% if messages %} + {% for message in messages %} + {% endfor %} -
- -
- - + {% endif %} + +
+ {% csrf_token %} + {{form|crispy}} +
+
+ +
+
-{% endblock %} \ No newline at end of file +{% endblock %} + +{% block customJS %} + +{% endblock %} diff --git a/templates/recruitment/applications_document_review_view.html b/templates/recruitment/applications_document_review_view.html index deae9b3..17bed49 100644 --- a/templates/recruitment/applications_document_review_view.html +++ b/templates/recruitment/applications_document_review_view.html @@ -286,7 +286,7 @@ {# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
-
- +
+
+ +
+ + + {% trans "Cancel" %} + + +
+ +
+
+
+
+
+ + +{% endblock %} diff --git a/templates/recruitment/source_form.html b/templates/recruitment/source_form.html index 0d76739..e2b0d07 100644 --- a/templates/recruitment/source_form.html +++ b/templates/recruitment/source_form.html @@ -2,196 +2,409 @@ {% load static i18n %} {% load widget_tweaks %} -{% block title %}{{ title }}{% endblock %} +{% block title %}{{ title }} - {{ block.super }}{% endblock %} + +{% block customCSS %} + +{% endblock %} {% block content %} -
-
-
-
-

{{ title }}

+
+
+ + + + + -
-
-
- {% csrf_token %} + {% if source %} + +
+
+
+
{% trans "Currently Editing" %}
+
+
+ +
+
+
{{ source.name }}
+ {% if source.source_type %} +

{% trans "Type" %}: {{ source.get_source_type_display }}

+ {% endif %} + {% if source.ip_address %} +

{% trans "IP Address" %}: {{ source.ip_address }}

+ {% endif %} + + {% trans "Created" %}: {{ source.created_at|date:"d M Y" }} • + {% trans "Last Updated" %}: {{ source.updated_at|date:"d M Y" }} + +
+
+
+
+
+ {% endif %} - {% if form.non_field_errors %} -
- {% for error in form.non_field_errors %} + +
+
+ {% if form.non_field_errors %} + + {% endif %} + + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + + {% csrf_token %} + +
+
+
+ + {{ form.name|add_class:"form-control" }} + {% if form.name.errors %} +
+ {% for error in form.name.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
{{ form.name.help_text }}
+
+
+
+
+ + {{ form.source_type|add_class:"form-select" }} + {% if form.source_type.errors %} +
+ {% for error in form.source_type.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
{{ form.source_type.help_text }}
+
+
+
+ +
+
+
+ + {{ form.ip_address|add_class:"form-control" }} + {% if form.ip_address.errors %} +
+ {% for error in form.ip_address.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
{{ form.ip_address.help_text }}
+
+
+
+
+ + {{ form.trusted_ips|add_class:"form-control" }} + {% if form.trusted_ips.errors %} +
+ {% for error in form.trusted_ips.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
{{ form.trusted_ips.help_text }}
+
+
+
+ +
+ + {{ form.description|add_class:"form-control" }} + {% if form.description.errors %} +
+ {% for error in form.description.errors %} {{ error }} {% endfor %}
{% endif %} +
{{ form.description.help_text }}
+
-
-
-
-