From 64e04a011d378a2d59bd4c6053d4b3458ca068d3 Mon Sep 17 00:00:00 2001 From: Faheed Date: Mon, 17 Nov 2025 13:49:50 +0300 Subject: [PATCH] scheduled interview --- .../__pycache__/models.cpython-312.pyc | Bin 107063 -> 107404 bytes recruitment/__pycache__/urls.cpython-312.pyc | Bin 21994 -> 23134 bytes recruitment/__pycache__/views.cpython-312.pyc | Bin 187287 -> 195534 bytes .../views_frontend.cpython-312.pyc | Bin 47019 -> 47114 bytes recruitment/migrations/0001_initial.py | 47 +- .../migrations/0002_jobposting_ai_parsed.py | 18 - .../0002_zoommeetingdetails_host_email.py | 18 - .../0003_add_agency_password_field.py | 18 - .../migrations/0004_alter_person_gender.py | 18 - recruitment/migrations/0005_person_gpa.py | 18 - .../0006_add_profile_fields_to_customuser.py | 24 - ...0007_migrate_profile_data_to_customuser.py | 60 -- .../migrations/0008_drop_profile_model.py | 16 - .../migrations/0009_alter_message_job.py | 20 - .../0010_add_document_review_stage.py | 18 - .../0011_add_document_review_stage.py | 13 - .../migrations/0012_application_exam_score.py | 18 - recruitment/models.py | 32 +- recruitment/urls.py | 117 ++-- recruitment/views.py | 547 +++++++++++++----- .../recruitment/candidate_interview_view.html | 21 +- 21 files changed, 529 insertions(+), 494 deletions(-) delete mode 100644 recruitment/migrations/0002_jobposting_ai_parsed.py delete mode 100644 recruitment/migrations/0002_zoommeetingdetails_host_email.py delete mode 100644 recruitment/migrations/0003_add_agency_password_field.py delete mode 100644 recruitment/migrations/0004_alter_person_gender.py delete mode 100644 recruitment/migrations/0005_person_gpa.py delete mode 100644 recruitment/migrations/0006_add_profile_fields_to_customuser.py delete mode 100644 recruitment/migrations/0007_migrate_profile_data_to_customuser.py delete mode 100644 recruitment/migrations/0008_drop_profile_model.py delete mode 100644 recruitment/migrations/0009_alter_message_job.py delete mode 100644 recruitment/migrations/0010_add_document_review_stage.py delete mode 100644 recruitment/migrations/0011_add_document_review_stage.py delete mode 100644 recruitment/migrations/0012_application_exam_score.py diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 1edec8c535bfc54050e86b607375d8679008c1b8..e14024eb18ada80bc4f2e1818f1e1def2e1add50 100644 GIT binary patch delta 5137 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 IoA3<&1Kel2p8x;= delta 5081 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 delta 5088 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 diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 2b263c2bbe78377b9733cbabbd036e1d5bbcd115..b69104f6a7cd26a8bacd076764ba478c9e689eea 100644 GIT binary patch delta 44652 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 LuwAT!@fY(yUJCc`26tQm3w)v| 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 delta 2211 zcmaJ?eQZ-z6z_fgZnti*bz`Eg`(V()J`_cCWM2WrzV4e4hVBZcYthjL1$*A|B|0ZRK5Md3QV4al^(jv@y77^eLnLSsMVME$pcg7i z&2YEfKqR!%MRoNGg@WFel%j@PMUqO=R_TI#3WcDM)KFoLhf8HT6ASQKxTqEeH^rBW zT2c3=a7rJhl{B+gQiujgD``aI5n)If^loO=icw)ovt7(pGR@NbrxY!k9-tJnpi*p@ zQp^ltxDu3kV&LeexMFdUxcC9|ar5atX)}akyrkWh4wG$h@IrljYxn@n^Y_gTAl4I} znXq7Huz|?T#QB-cORP!W%o5vnvBm+v)xRolZg3J32VXU=C|<`d2kmFIQV&`=2=xe8 z(5FIZ#AOqL3c-t@!GM#2RR@nX6@c3p2VG4`hE^6F78DBi2^en5t-PtycVv#M%=pgO zU52ma2eUh~$5lywt!eMh_MIOje6LOR8_n!(O!lW|c9f1srT9~`2Ui?lb#&FSY+NV% zqs#}(JIhaGonJa;-Oy1!9`&qT+AImJZCFb?!Y+gZSQYPsx9|@7b|bvYz^bSF(C$Lm zkI>D)YuwOetF>(Q*gW-aubwZ~+dK|>05c9E^dKl0SPe8%a7|D#G@y{;9^AAyxM{8| zc%_WW5dV9a`aS}|eEl4ephjr)ks1qh1=G@_t#j!4cIE%m^Fxhfrqg)53lB|S;-n7C zoH=11Ui~JQbGv0d^tB{t8nCBkS#F7s*7TuW0zEr2U}~S89D*%f7Gu~nVOLjL0YA0S zG$VTskY%C6q_gw&3ac!%1KB&_R@d3pVVvmXSHc~6FXk%1mHqiCNEk4ATy<5etEwH% zjV>y>1FCJTn;G`y-6`z5SlazW%28xKgm8{A2b9#+7*KC@I%>p#+HH4H2OQ~6H3o}f zS#C45*_Obs-H)ixvE`(!Jg`znMj)p*i(G&=dN-2u@KNv6W(gUOW3A`WqG*l4RBw)Y zl&xFj<$a^VlNT{`389F^%jwr>e#5|O2qf6qZ`>)e>s#e+-fpjQJ7|N`?x0s8?eJR7 zx0v}I*bXNYUS(!L<95|kyCV=4k$VlZE;Ddz8NVg?&@!y$I))H@@)w5%RU)QZwX_gp z7cj>6j=)%dF1ZL~Ad6groPk99IL2BrYa;^xn8% z1ihT+23m#a3rSuku)}AD3j>SQKe6zrd}E+TNq&H>#~#=Gg85%U|FLbxpV7RDFb;9P z{D=6wxsx;VdEdhD?@;Zts&6y%Z}~%CzDl!d zyoCG-dBa`baa-iEr*{&)qy$+RB%eijyFN+o}A^-pY 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%}