diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 1edec8c..e14024e 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 1c54c1d..30fcc8d 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 2b263c2..b69104f 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index c5f727d..14b1e27 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index e11b731..db362f9 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-11-14 21:43 +# Generated by Django 5.2.7 on 2025-11-17 09:52 import django.contrib.auth.models import django.contrib.auth.validators @@ -127,6 +127,8 @@ class Migration(migrations.Migration): ('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')), ], @@ -221,6 +223,7 @@ class Migration(migrations.Migration): ('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={ @@ -241,10 +244,11 @@ class Migration(migrations.Migration): ('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'), ('Offer', 'Offer'), ('Hired', 'Hired'), ('Rejected', 'Rejected')], db_index=True, default='Applied', max_length=20, verbose_name='Stage')), + ('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')), @@ -289,6 +293,7 @@ class Migration(migrations.Migration): ('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')), ], @@ -337,6 +342,7 @@ class Migration(migrations.Migration): ('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')), @@ -428,7 +434,7 @@ class Migration(migrations.Migration): ('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(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.jobposting', verbose_name='Related Job')), + ('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')), ], @@ -450,7 +456,6 @@ class Migration(migrations.Migration): ('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')), - ('inteview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='recruitment.interviewschedule', verbose_name='Related Interview')), ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), ], options={ @@ -472,7 +477,8 @@ class Migration(migrations.Migration): ('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'), ('O', 'Other'), ('P', 'Prefer not to say')], max_length=1, null=True, verbose_name='Gender')), + ('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')), @@ -490,16 +496,6 @@ class Migration(migrations.Migration): name='person', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.person', verbose_name='Person'), ), - migrations.CreateModel( - name='Profile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])), - ('designation', models.CharField(blank=True, max_length=100, null=True)), - ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), - ], - ), migrations.CreateModel( name='ScheduledInterview', fields=[ @@ -660,6 +656,11 @@ class Migration(migrations.Migration): model_name='formsubmission', index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), ), + migrations.AddField( + model_name='notification', + name='related_meeting', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='recruitment.zoommeetingdetails', verbose_name='Related Meeting'), + ), migrations.AddIndex( model_name='formtemplate', index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), @@ -704,14 +705,6 @@ class Migration(migrations.Migration): model_name='message', index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'), ), - migrations.AddIndex( - model_name='notification', - index=models.Index(fields=['status', 'scheduled_for'], name='recruitment_status_0ebbe4_idx'), - ), - migrations.AddIndex( - model_name='notification', - index=models.Index(fields=['recipient'], name='recruitment_recipie_eadf4c_idx'), - ), migrations.AddIndex( model_name='person', index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'), @@ -764,4 +757,12 @@ class Migration(migrations.Migration): model_name='jobposting', index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), ), + migrations.AddIndex( + model_name='notification', + index=models.Index(fields=['status', 'scheduled_for'], name='recruitment_status_0ebbe4_idx'), + ), + migrations.AddIndex( + model_name='notification', + index=models.Index(fields=['recipient'], name='recruitment_recipie_eadf4c_idx'), + ), ] diff --git a/recruitment/migrations/0002_jobposting_ai_parsed.py b/recruitment/migrations/0002_jobposting_ai_parsed.py deleted file mode 100644 index af9eade..0000000 --- a/recruitment/migrations/0002_jobposting_ai_parsed.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-13 13:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='ai_parsed', - field=models.BooleanField(default=False, help_text='Whether the job posting has been parsed by AI', verbose_name='AI Parsed'), - ), - ] diff --git a/recruitment/migrations/0002_zoommeetingdetails_host_email.py b/recruitment/migrations/0002_zoommeetingdetails_host_email.py deleted file mode 100644 index 6425f6a..0000000 --- a/recruitment/migrations/0002_zoommeetingdetails_host_email.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-14 22:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='zoommeetingdetails', - name='host_email', - field=models.CharField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0003_add_agency_password_field.py b/recruitment/migrations/0003_add_agency_password_field.py deleted file mode 100644 index fca0d6b..0000000 --- a/recruitment/migrations/0003_add_agency_password_field.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-13 14:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_jobposting_ai_parsed'), - ] - - operations = [ - migrations.AddField( - model_name='hiringagency', - name='generated_password', - field=models.CharField(blank=True, help_text='Generated password for agency user account', max_length=255, null=True), - ), - ] diff --git a/recruitment/migrations/0004_alter_person_gender.py b/recruitment/migrations/0004_alter_person_gender.py deleted file mode 100644 index eee5da4..0000000 --- a/recruitment/migrations/0004_alter_person_gender.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-14 23:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_add_agency_password_field'), - ] - - operations = [ - migrations.AlterField( - model_name='person', - name='gender', - field=models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender'), - ), - ] diff --git a/recruitment/migrations/0005_person_gpa.py b/recruitment/migrations/0005_person_gpa.py deleted file mode 100644 index 19dd0ad..0000000 --- a/recruitment/migrations/0005_person_gpa.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-15 20:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_alter_person_gender'), - ] - - operations = [ - migrations.AddField( - model_name='person', - name='gpa', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='GPA'), - ), - ] diff --git a/recruitment/migrations/0006_add_profile_fields_to_customuser.py b/recruitment/migrations/0006_add_profile_fields_to_customuser.py deleted file mode 100644 index a8342d6..0000000 --- a/recruitment/migrations/0006_add_profile_fields_to_customuser.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-15 20:56 - -import recruitment.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_person_gpa'), - ] - - operations = [ - migrations.AddField( - model_name='customuser', - name='designation', - field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation'), - ), - migrations.AddField( - model_name='customuser', - name='profile_image', - field=models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image'), - ), - ] diff --git a/recruitment/migrations/0007_migrate_profile_data_to_customuser.py b/recruitment/migrations/0007_migrate_profile_data_to_customuser.py deleted file mode 100644 index 475ef68..0000000 --- a/recruitment/migrations/0007_migrate_profile_data_to_customuser.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-15 20:57 - -from django.db import migrations - - -def migrate_profile_data_to_customuser(apps, schema_editor): - """ - Migrate data from Profile model to CustomUser model - """ - CustomUser = apps.get_model('recruitment', 'CustomUser') - Profile = apps.get_model('recruitment', 'Profile') - - # Get all profiles - profiles = Profile.objects.all() - - for profile in profiles: - if profile.user: - # Update CustomUser with Profile data - user = profile.user - if profile.profile_image: - user.profile_image = profile.profile_image - if profile.designation: - user.designation = profile.designation - user.save(update_fields=['profile_image', 'designation']) - - -def reverse_migrate_profile_data(apps, schema_editor): - """ - Reverse migration: move data from CustomUser back to Profile - """ - CustomUser = apps.get_model('recruitment', 'CustomUser') - Profile = apps.get_model('recruitment', 'Profile') - - # Get all users with profile data - users = CustomUser.objects.exclude(profile_image__isnull=True).exclude(profile_image='') - - for user in users: - # Get or create profile for this user - profile, created = Profile.objects.get_or_create(user=user) - - # Update Profile with CustomUser data - if user.profile_image: - profile.profile_image = user.profile_image - if user.designation: - profile.designation = user.designation - profile.save(update_fields=['profile_image', 'designation']) - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_add_profile_fields_to_customuser'), - ] - - operations = [ - migrations.RunPython( - migrate_profile_data_to_customuser, - reverse_migrate_profile_data, - ), - ] diff --git a/recruitment/migrations/0008_drop_profile_model.py b/recruitment/migrations/0008_drop_profile_model.py deleted file mode 100644 index 376ed4a..0000000 --- a/recruitment/migrations/0008_drop_profile_model.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated manually to drop the Profile model after migration to CustomUser - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_migrate_profile_data_to_customuser'), - ] - - operations = [ - migrations.DeleteModel( - name='Profile', - ), - ] diff --git a/recruitment/migrations/0009_alter_message_job.py b/recruitment/migrations/0009_alter_message_job.py deleted file mode 100644 index e93abc0..0000000 --- a/recruitment/migrations/0009_alter_message_job.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-16 10:00 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_drop_profile_model'), - ] - - operations = [ - migrations.AlterField( - model_name='message', - name='job', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.jobposting', verbose_name='Related Job'), - preserve_default=False, - ), - ] diff --git a/recruitment/migrations/0010_add_document_review_stage.py b/recruitment/migrations/0010_add_document_review_stage.py deleted file mode 100644 index 30ffde1..0000000 --- a/recruitment/migrations/0010_add_document_review_stage.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-16 11:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0009_alter_message_job'), - ] - - operations = [ - migrations.AlterField( - model_name='application', - name='stage', - field=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'), - ), - ] diff --git a/recruitment/migrations/0011_add_document_review_stage.py b/recruitment/migrations/0011_add_document_review_stage.py deleted file mode 100644 index 6529b84..0000000 --- a/recruitment/migrations/0011_add_document_review_stage.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-16 12:08 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0010_add_document_review_stage'), - ] - - operations = [ - ] diff --git a/recruitment/migrations/0012_application_exam_score.py b/recruitment/migrations/0012_application_exam_score.py deleted file mode 100644 index 8a4b146..0000000 --- a/recruitment/migrations/0012_application_exam_score.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-16 12:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0011_add_document_review_stage'), - ] - - operations = [ - migrations.AddField( - model_name='application', - name='exam_score', - field=models.FloatField(blank=True, null=True, verbose_name='Exam Score'), - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index e3c33cc..84210f0 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -906,11 +906,35 @@ class Application(Base): @property def get_latest_meeting(self): - """Legacy compatibility - get latest meeting for this application""" + """ + 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() - if schedule: - return schedule.zoom_meeting - 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 + + # 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' + + # 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 + @property def has_future_meeting(self): diff --git a/recruitment/urls.py b/recruitment/urls.py index 1e10439..7e9fbf5 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -35,16 +35,7 @@ urlpatterns = [ ), path("jobs/linkedin/login/", views.linkedin_login, name="linkedin_login"), path("jobs/linkedin/callback/", views.linkedin_callback, name="linkedin_callback"), - 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", - ), + # Candidate URLs path( "candidates/", views_frontend.ApplicationListView.as_view(), name="candidate_list" @@ -299,38 +290,7 @@ urlpatterns = [ views.interview_detail_view, name="interview_detail", ), - # Candidate Meeting Scheduling/Rescheduling URLs - path( - "jobs//candidates//schedule-meeting/", - views.schedule_candidate_meeting, - name="schedule_candidate_meeting", - ), - path( - "api/jobs//candidates//schedule-meeting/", - views.api_schedule_candidate_meeting, - name="api_schedule_candidate_meeting", - ), - path( - "jobs//candidates//reschedule-meeting//", - views.reschedule_candidate_meeting, - name="reschedule_candidate_meeting", - ), - path( - "api/jobs//candidates//reschedule-meeting//", - views.api_reschedule_candidate_meeting, - name="api_reschedule_candidate_meeting", - ), - # New URL for simple page-based meeting scheduling - path( - "jobs//candidates//schedule-meeting-page/", - views.schedule_meeting_for_candidate, - name="schedule_meeting_for_candidate", - ), - path( - "jobs//candidates//delete_meeting_for_candidate//", - views.delete_meeting_for_candidate, - name="delete_meeting_for_candidate", - ), + # users urls path("user/", views.user_detail, name="user_detail"), path( @@ -623,4 +583,77 @@ urlpatterns = [ # path('interviews//', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'), # path('interviews//update/', views.ScheduledInterviewUpdateView.as_view(), name='update_scheduled_interview'), # 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", + ), + + # Candidate Meeting Scheduling/Rescheduling URLs + path( + "jobs//candidates//schedule-meeting/", + views.schedule_candidate_meeting, + name="schedule_candidate_meeting", + ), + path( + "api/jobs//candidates//schedule-meeting/", + views.api_schedule_candidate_meeting, + name="api_schedule_candidate_meeting", + ), + path( + "jobs//candidates//reschedule-meeting//", + views.reschedule_candidate_meeting, + name="reschedule_candidate_meeting", + ), + path( + "api/jobs//candidates//reschedule-meeting//", + views.api_reschedule_candidate_meeting, + name="api_reschedule_candidate_meeting", + ), + # New URL for simple page-based meeting scheduling + path( + "jobs//candidates//schedule-meeting-page/", + views.schedule_meeting_for_candidate, + name="schedule_meeting_for_candidate", + ), + path( + "jobs//candidates//delete_meeting_for_candidate//", + views.delete_meeting_for_candidate, + name="delete_meeting_for_candidate", + ), + + + path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"), + + # 1. Onsite Reschedule URL + path( + '/candidate//onsite/reschedule//', + views.reschedule_onsite_meeting, + name='reschedule_onsite_meeting' + ), + + # 2. Onsite Delete URL + + path( + 'job//candidates//delete-onsite-meeting//', + views.delete_onsite_meeting_for_candidate, + name='delete_onsite_meeting_for_candidate' + ), + + path( + 'job//candidate//schedule/onsite/', + views.schedule_onsite_meeting_for_candidate, + name='schedule_onsite_meeting_for_candidate' # This is the name used in the button + ), + + + # Detail View (assuming slug is on ScheduledInterview) + # path("interviews/meetings//", views.MeetingDetailView.as_view(), name="meeting_details"), ] diff --git a/recruitment/views.py b/recruitment/views.py index ebeb48e..63289b0 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -75,6 +75,10 @@ from .forms import ( PortalLoginForm, MessageForm, PersonForm, + OnsiteMeetingForm, + OnsiteReshuduleForm, + OnsiteScheduleForm, + InterviewEmailForm ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from rest_framework import viewsets @@ -116,7 +120,7 @@ from .models import ( JobPosting, ScheduledInterview, JobPostingImage, - MeetingComment, + HiringAgency, AgencyJobAssignment, AgencyAccessLink, @@ -127,6 +131,8 @@ from .models import ( OnsiteLocationDetails, InterviewLocation ) + + import logging from datastar_py.django import ( DatastarResponse, @@ -258,9 +264,7 @@ class ZoomMeetingListView(StaffRequiredMixin, ListView): queryset = queryset.prefetch_related( Prefetch( "interview", # related_name from ZoomMeeting to ScheduledInterview - queryset=ScheduledInterview.objects.select_related( - "application", "job" - ), + queryset=ScheduledInterview.objects.select_related("application", "job"), to_attr="interview_details", # Changed to not start with underscore ) ) @@ -298,6 +302,7 @@ class ZoomMeetingListView(StaffRequiredMixin, ListView): return context + # @login_required # def InterviewListView(request): # # interview_type=request.GET.get('interview_type','Remote') @@ -468,7 +473,6 @@ def ZoomMeetingDeleteView(request, slug): messages.error(request, str(e)) return redirect(reverse("list_meetings")) - # Job Posting # def job_list(request): # """Display the list of job postings order by creation date descending""" @@ -1504,6 +1508,7 @@ def _handle_get_request(request, slug, job): ) + def _handle_preview_submission(request, slug, job): """ Handles the initial POST request (Preview Schedule). @@ -1516,7 +1521,6 @@ def _handle_preview_submission(request, slug, job): if form.is_valid(): # Get the form data applications = form.cleaned_data["applications"] - interview_type = form.cleaned_data["interview_type"] start_date = form.cleaned_data["start_date"] end_date = form.cleaned_data["end_date"] working_days = form.cleaned_data["working_days"] @@ -1572,16 +1576,11 @@ def _handle_preview_submission(request, slug, job): for i, application in enumerate(applications): slot = available_slots[i] preview_schedule.append( - { - "applications": applications, - "date": slot["date"], - "time": slot["time"], - } + {"application": application, "date": slot["date"], "time": slot["time"]} ) # Save the form data to session for later use schedule_data = { - "interview_type": interview_type, "start_date": start_date.isoformat(), "end_date": end_date.isoformat(), "working_days": working_days, @@ -1604,7 +1603,6 @@ def _handle_preview_submission(request, slug, job): { "job": job, "schedule": preview_schedule, - "interview_type": interview_type, "start_date": start_date, "end_date": end_date, "working_days": working_days, @@ -1842,13 +1840,12 @@ def _handle_confirm_schedule(request, slug, job): # 3. Setup candidates and get slots candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"]) - schedule.candidates.set(candidates) - available_slots = get_available_time_slots( - schedule - ) # This should still be synchronous and fast + schedule.applications.set(candidates) + available_slots = get_available_time_slots(schedule) - # 4. Queue scheduled interviews asynchronously (FAST RESPONSE) - if schedule.interview_type == "Remote": + # 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): @@ -1869,27 +1866,79 @@ def _handle_confirm_schedule(request, slug, job): if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY] return redirect("job_detail", slug=slug) - else: - for i, candidate in enumerate(candidates): - if i < len(available_slots): - slot = available_slots[i] - ScheduledInterview.objects.create( - candidate=candidate, - job=job, - # zoom_meeting=None, - schedule=schedule, - interview_date=slot["date"], - interview_time=slot["time"], - ) - messages.success(request, f"Onsite schedule Interview Create succesfully") + elif schedule_data.get("schedule_interview_type") == 'Onsite': + print("inside...") + + if request.method == 'POST': + form = OnsiteMeetingForm(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'] + + 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" + + ) + + # 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 = OnsiteMeetingForm() + + return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job}) - # Clear both 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("schedule_interview_location_form", slug=schedule.slug) def schedule_interviews_view(request, slug): @@ -2135,41 +2184,18 @@ def candidate_update_status(request, slug): def candidate_interview_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) - if request.method == "POST": - form = ParticipantsSelectForm(request.POST, instance=job) - - if form.is_valid(): - # Save the main instance (JobPosting) - job_instance = form.save(commit=False) - job_instance.save() - - # MANUALLY set the M2M relationships based on submitted data - job_instance.participants.set(form.cleaned_data["participants"]) - job_instance.users.set(form.cleaned_data["users"]) - - messages.success(request, "Interview participants updated successfully.") - return redirect("candidate_interview_view", slug=job.slug) - - else: - initial_data = { - "participants": job.participants.all(), - "users": job.users.all(), - } - form = ParticipantsSelectForm(instance=job, initial=initial_data) - - else: - form = ParticipantsSelectForm(instance=job) context = { "job": job, "candidates": job.interview_candidates, "current_stage": "Interview", - "form": form, - "participants_count": 0 #job.participants.count() + job.users.count(), + } return render(request, "recruitment/candidate_interview_view.html", context) + + @staff_user_required def candidate_document_review_view(request, slug): """ @@ -5332,77 +5358,39 @@ def compose_candidate_email(request, job_slug): from .email_service import send_bulk_email job = get_object_or_404(JobPosting, slug=job_slug) - candidate = get_object_or_404(Application, slug=candidate_slug, job=job) - if request.method == "POST": - form = CandidateEmailForm(job, candidate, request.POST) - candidate_ids = request.GET.getlist("candidate_ids") - candidates = Application.objects.filter(id__in=candidate_ids) + + # # candidate = get_object_or_404(Application, slug=candidate_slug, job=job) + # if request.method == "POST": + # form = CandidateEmailForm(job, candidate, request.POST) + candidate_ids=request.GET.getlist('candidate_ids') + candidates=Application.objects.filter(id__in=candidate_ids) - if request.method == "POST": - print( - "........................................................inside candidate conpose............." - ) - candidate_ids = request.POST.getlist("candidate_ids") - candidates = Application.objects.filter(id__in=candidate_ids) + + if request.method == 'POST': + print("........................................................inside candidate conpose.............") + candidate_ids = request.POST.getlist('candidate_ids') + candidates=Application.objects.filter(id__in=candidate_ids) form = CandidateEmailForm(job, candidates, request.POST) if form.is_valid(): print("form is valid ...") # Get email addresses email_addresses = form.get_email_addresses() - if not email_addresses: - messages.error( - request, "No valid email addresses found for selected recipients." - ) - return render( - request, - "includes/email_compose_form.html", - {"form": form, "job": job, "candidate": candidate}, - ) - - # Check if this is an interview invitation - subject = form.cleaned_data.get("subject", "").lower() - is_interview_invitation = "interview" in subject or "meeting" in subject - - if is_interview_invitation: - # Use HTML template for interview invitations - meeting_details = None - if form.cleaned_data.get("include_meeting_details"): - # Try to get meeting details from candidate - meeting_details = { - "topic": f"Interview for {job.title}", - "date_time": getattr( - candidate, "interview_date", "To be scheduled" - ), - "duration": "60 minutes", - "join_url": getattr(candidate, "meeting_url", ""), - } - - from .email_service import send_interview_invitation_email - - email_result = send_interview_invitation_email( - candidate=candidate, - job=job, - meeting_details=meeting_details, - recipient_list=email_addresses, - ) - else: - # Get formatted message for regular emails - message = form.get_formatted_message() - subject = form.cleaned_data.get("subject") - print(email_addresses) + if not email_addresses: - messages.error(request, "No email selected") - referer = request.META.get("HTTP_REFERER") + messages.error(request, 'No email selected') + referer = request.META.get('HTTP_REFERER') if referer: # Redirect back to the referring page return redirect(referer) else: - return redirect("dashboard") + + return redirect('dashboard') + message = form.get_formatted_message() - subject = form.cleaned_data.get("subject") + subject = form.cleaned_data.get('subject') # Send emails using email service (no attachments, synchronous to avoid pickle issues) @@ -5413,7 +5401,7 @@ def compose_candidate_email(request, job_slug): request=request, attachments=None, async_task_=True, # Changed to False to avoid pickle issues - from_interview=False, + from_interview=False ) if email_result["success"]: @@ -5441,34 +5429,17 @@ def compose_candidate_email(request, job_slug): } ) - return render( - request, - "includes/email_compose_form.html", - {"form": form, "job": job, "candidate": candidate}, - ) - return render( request, "includes/email_compose_form.html", {"form": form, "job": job, "candidate": candidates}, ) - # except Exception as e: - # logger.error(f"Error sending candidate email: {e}") - # messages.error(request, f'An error occurred while sending the email: {str(e)}') - - # # For HTMX requests, return error response - # if 'HX-Request' in request.headers: - # return JsonResponse({ - # 'success': False, - # 'error': f'An error occurred while sending the email: {str(e)}' - # }) - else: # Form validation errors - print("form is not valid") + print('form is not valid') print(form.errors) messages.error(request, "Please correct the errors below.") @@ -5484,9 +5455,8 @@ def compose_candidate_email(request, job_slug): return render( request, "includes/email_compose_form.html", - {"form": form, "job": job, "candidates": candidate}, - s, - ) + {"form": form, "job": job, "candidates": candidates}, + ) else: # GET request - show the form @@ -5500,6 +5470,7 @@ def compose_candidate_email(request, job_slug): ) + # Source CRUD Views @staff_user_required def source_list(request): @@ -5844,12 +5815,288 @@ def send_interview_email(request, slug): # return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule}) -def onsite_interview_list_view(request): - onsite_interviews = ScheduledInterview.objects.filter( - schedule__interview_type="Onsite" - ) - return render( - request, - "interviews/onsite_interview_list.html", - {"onsite_interviews": onsite_interviews}, +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', + ) + # 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) + + # 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() + print(normalized_type) + # Assuming InterviewLocation.LocationType is accessible/defined + if normalized_type in ['Remote', 'Onsite']: + queryset = queryset.filter(interview_location__location_type=normalized_type) + print(queryset) + + # 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) + + # 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") + + + 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", "") + + + # CORRECTED: Pass the status choices from the model class for the filter dropdown + context["status_choices"] = self.model.InterviewStatus.choices + + meetings_data = [] + + for interview in context.get(self.context_object_name, []): + location = interview.interview_location + details = None + + 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) + + # 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, + }) + + context["meetings_data"] = meetings_data + + return context + +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 ) + + if request.method == 'POST': + form = OnsiteReshuduleForm(request.POST, instance=onsite_meeting) + + 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}) + + # 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! ✅") + + return redirect(reverse("candidate_interview_view", kwargs={'slug': job.slug})) + + 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) + + +# recruitment/views.py + +@staff_user_required +def delete_onsite_meeting_for_candidate(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("candidate_interview_view", kwargs={"slug": job.slug})) + + context = { + "job": job, + "candidate": candidate, + "meeting": meeting, + "location_type": "Onsite", + "delete_url": reverse( + "delete_onsite_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) + + + +def schedule_onsite_meeting_for_candidate(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_candidate', + 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("candidate_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) +# def meeting_list_view(request): +# queryset = ScheduledInterview.filter(interview_location__isnull=False).select_related( +# 'interview_location', +# 'job', +# 'application__person', +# 'application', +# ).prefetch_related( +# 'interview_location__zoommeetingdetails', +# 'interview_location__onsitelocationdetails', +# ) +# print(queryset) +# return render(request,) +# ========================================================================= +# 2. Simple Meeting Creation Views (Placeholders) +# ========================================================================= diff --git a/templates/recruitment/candidate_interview_view.html b/templates/recruitment/candidate_interview_view.html index 9152efc..fb6a4fa 100644 --- a/templates/recruitment/candidate_interview_view.html +++ b/templates/recruitment/candidate_interview_view.html @@ -206,14 +206,11 @@ {% csrf_token %} {# Select Input Group - No label needed for this one, so we just flex the select and button #} - + + -<<<<<<< HEAD {% else%}