scheduled interview
This commit is contained in:
parent
d0235bfefe
commit
64e04a011d
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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.models
|
||||||
import django.contrib.auth.validators
|
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')),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
|
||||||
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
|
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
|
||||||
('address', models.TextField(blank=True, 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')),
|
('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={
|
options={
|
||||||
@ -241,10 +244,11 @@ class Migration(migrations.Migration):
|
|||||||
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
|
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
|
||||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
('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')),
|
('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_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_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_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')),
|
('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Interview Status')),
|
||||||
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
|
('offer_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')),
|
('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')),
|
||||||
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
|
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
|
||||||
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
|
('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')),
|
('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')),
|
||||||
('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')),
|
('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')),
|
('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_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)),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
|
||||||
('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')),
|
('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')),
|
('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')),
|
('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)),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('attempts', models.PositiveIntegerField(default=0, verbose_name='Send Attempts')),
|
('attempts', models.PositiveIntegerField(default=0, verbose_name='Send Attempts')),
|
||||||
('last_error', models.TextField(blank=True, verbose_name='Last Error Message')),
|
('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')),
|
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
|
||||||
],
|
],
|
||||||
options={
|
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')),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('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',
|
name='person',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.person', verbose_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(
|
migrations.CreateModel(
|
||||||
name='ScheduledInterview',
|
name='ScheduledInterview',
|
||||||
fields=[
|
fields=[
|
||||||
@ -660,6 +656,11 @@ class Migration(migrations.Migration):
|
|||||||
model_name='formsubmission',
|
model_name='formsubmission',
|
||||||
index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'),
|
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(
|
migrations.AddIndex(
|
||||||
model_name='formtemplate',
|
model_name='formtemplate',
|
||||||
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
|
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
|
||||||
@ -704,14 +705,6 @@ class Migration(migrations.Migration):
|
|||||||
model_name='message',
|
model_name='message',
|
||||||
index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'),
|
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(
|
migrations.AddIndex(
|
||||||
model_name='person',
|
model_name='person',
|
||||||
index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'),
|
index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'),
|
||||||
@ -764,4 +757,12 @@ class Migration(migrations.Migration):
|
|||||||
model_name='jobposting',
|
model_name='jobposting',
|
||||||
index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'),
|
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'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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 = [
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -906,11 +906,35 @@ class Application(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def get_latest_meeting(self):
|
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()
|
schedule = self.scheduled_interviews.order_by("-created_at").first()
|
||||||
if schedule:
|
|
||||||
return schedule.zoom_meeting
|
# Check if a schedule exists and if it has an interview location
|
||||||
return None
|
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
|
@property
|
||||||
def has_future_meeting(self):
|
def has_future_meeting(self):
|
||||||
|
|||||||
@ -35,16 +35,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("jobs/linkedin/login/", views.linkedin_login, name="linkedin_login"),
|
path("jobs/linkedin/login/", views.linkedin_login, name="linkedin_login"),
|
||||||
path("jobs/linkedin/callback/", views.linkedin_callback, name="linkedin_callback"),
|
path("jobs/linkedin/callback/", views.linkedin_callback, name="linkedin_callback"),
|
||||||
path(
|
|
||||||
"jobs/<slug:slug>/schedule-interviews/",
|
|
||||||
views.schedule_interviews_view,
|
|
||||||
name="schedule_interviews",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"jobs/<slug:slug>/confirm-schedule-interviews/",
|
|
||||||
views.confirm_schedule_interviews_view,
|
|
||||||
name="confirm_schedule_interviews_view",
|
|
||||||
),
|
|
||||||
# Candidate URLs
|
# Candidate URLs
|
||||||
path(
|
path(
|
||||||
"candidates/", views_frontend.ApplicationListView.as_view(), name="candidate_list"
|
"candidates/", views_frontend.ApplicationListView.as_view(), name="candidate_list"
|
||||||
@ -299,38 +290,7 @@ urlpatterns = [
|
|||||||
views.interview_detail_view,
|
views.interview_detail_view,
|
||||||
name="interview_detail",
|
name="interview_detail",
|
||||||
),
|
),
|
||||||
# Candidate Meeting Scheduling/Rescheduling URLs
|
|
||||||
path(
|
|
||||||
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
|
|
||||||
views.schedule_candidate_meeting,
|
|
||||||
name="schedule_candidate_meeting",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
|
|
||||||
views.api_schedule_candidate_meeting,
|
|
||||||
name="api_schedule_candidate_meeting",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
|
|
||||||
views.reschedule_candidate_meeting,
|
|
||||||
name="reschedule_candidate_meeting",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
|
|
||||||
views.api_reschedule_candidate_meeting,
|
|
||||||
name="api_reschedule_candidate_meeting",
|
|
||||||
),
|
|
||||||
# New URL for simple page-based meeting scheduling
|
|
||||||
path(
|
|
||||||
"jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/",
|
|
||||||
views.schedule_meeting_for_candidate,
|
|
||||||
name="schedule_meeting_for_candidate",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"jobs/<slug:slug>/candidates/<int:candidate_pk>/delete_meeting_for_candidate/<int:meeting_id>/",
|
|
||||||
views.delete_meeting_for_candidate,
|
|
||||||
name="delete_meeting_for_candidate",
|
|
||||||
),
|
|
||||||
# users urls
|
# users urls
|
||||||
path("user/<int:pk>", views.user_detail, name="user_detail"),
|
path("user/<int:pk>", views.user_detail, name="user_detail"),
|
||||||
path(
|
path(
|
||||||
@ -623,4 +583,77 @@ urlpatterns = [
|
|||||||
# path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'),
|
# path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'),
|
||||||
# path('interviews/<slug:slug>/update/', views.ScheduledInterviewUpdateView.as_view(), name='update_scheduled_interview'),
|
# path('interviews/<slug:slug>/update/', views.ScheduledInterviewUpdateView.as_view(), name='update_scheduled_interview'),
|
||||||
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
||||||
|
|
||||||
|
#interview and meeting related urls
|
||||||
|
path(
|
||||||
|
"jobs/<slug:slug>/schedule-interviews/",
|
||||||
|
views.schedule_interviews_view,
|
||||||
|
name="schedule_interviews",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"jobs/<slug:slug>/confirm-schedule-interviews/",
|
||||||
|
views.confirm_schedule_interviews_view,
|
||||||
|
name="confirm_schedule_interviews_view",
|
||||||
|
),
|
||||||
|
|
||||||
|
# Candidate Meeting Scheduling/Rescheduling URLs
|
||||||
|
path(
|
||||||
|
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
|
||||||
|
views.schedule_candidate_meeting,
|
||||||
|
name="schedule_candidate_meeting",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
|
||||||
|
views.api_schedule_candidate_meeting,
|
||||||
|
name="api_schedule_candidate_meeting",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
|
||||||
|
views.reschedule_candidate_meeting,
|
||||||
|
name="reschedule_candidate_meeting",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
|
||||||
|
views.api_reschedule_candidate_meeting,
|
||||||
|
name="api_reschedule_candidate_meeting",
|
||||||
|
),
|
||||||
|
# New URL for simple page-based meeting scheduling
|
||||||
|
path(
|
||||||
|
"jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/",
|
||||||
|
views.schedule_meeting_for_candidate,
|
||||||
|
name="schedule_meeting_for_candidate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"jobs/<slug:slug>/candidates/<int:candidate_pk>/delete_meeting_for_candidate/<int:meeting_id>/",
|
||||||
|
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(
|
||||||
|
'<slug:slug>/candidate/<int:candidate_id>/onsite/reschedule/<int:meeting_id>/',
|
||||||
|
views.reschedule_onsite_meeting,
|
||||||
|
name='reschedule_onsite_meeting'
|
||||||
|
),
|
||||||
|
|
||||||
|
# 2. Onsite Delete URL
|
||||||
|
|
||||||
|
path(
|
||||||
|
'job/<slug:slug>/candidates/<int:candidate_pk>/delete-onsite-meeting/<int:meeting_id>/',
|
||||||
|
views.delete_onsite_meeting_for_candidate,
|
||||||
|
name='delete_onsite_meeting_for_candidate'
|
||||||
|
),
|
||||||
|
|
||||||
|
path(
|
||||||
|
'job/<slug:slug>/candidate/<int:candidate_pk>/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/<slug:slug>/", views.MeetingDetailView.as_view(), name="meeting_details"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -75,6 +75,10 @@ from .forms import (
|
|||||||
PortalLoginForm,
|
PortalLoginForm,
|
||||||
MessageForm,
|
MessageForm,
|
||||||
PersonForm,
|
PersonForm,
|
||||||
|
OnsiteMeetingForm,
|
||||||
|
OnsiteReshuduleForm,
|
||||||
|
OnsiteScheduleForm,
|
||||||
|
InterviewEmailForm
|
||||||
)
|
)
|
||||||
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
@ -116,7 +120,7 @@ from .models import (
|
|||||||
JobPosting,
|
JobPosting,
|
||||||
ScheduledInterview,
|
ScheduledInterview,
|
||||||
JobPostingImage,
|
JobPostingImage,
|
||||||
MeetingComment,
|
|
||||||
HiringAgency,
|
HiringAgency,
|
||||||
AgencyJobAssignment,
|
AgencyJobAssignment,
|
||||||
AgencyAccessLink,
|
AgencyAccessLink,
|
||||||
@ -127,6 +131,8 @@ from .models import (
|
|||||||
OnsiteLocationDetails,
|
OnsiteLocationDetails,
|
||||||
InterviewLocation
|
InterviewLocation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datastar_py.django import (
|
from datastar_py.django import (
|
||||||
DatastarResponse,
|
DatastarResponse,
|
||||||
@ -258,9 +264,7 @@ class ZoomMeetingListView(StaffRequiredMixin, ListView):
|
|||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"interview", # related_name from ZoomMeeting to ScheduledInterview
|
"interview", # related_name from ZoomMeeting to ScheduledInterview
|
||||||
queryset=ScheduledInterview.objects.select_related(
|
queryset=ScheduledInterview.objects.select_related("application", "job"),
|
||||||
"application", "job"
|
|
||||||
),
|
|
||||||
to_attr="interview_details", # Changed to not start with underscore
|
to_attr="interview_details", # Changed to not start with underscore
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -298,6 +302,7 @@ class ZoomMeetingListView(StaffRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @login_required
|
# @login_required
|
||||||
# def InterviewListView(request):
|
# def InterviewListView(request):
|
||||||
# # interview_type=request.GET.get('interview_type','Remote')
|
# # interview_type=request.GET.get('interview_type','Remote')
|
||||||
@ -468,7 +473,6 @@ def ZoomMeetingDeleteView(request, slug):
|
|||||||
messages.error(request, str(e))
|
messages.error(request, str(e))
|
||||||
return redirect(reverse("list_meetings"))
|
return redirect(reverse("list_meetings"))
|
||||||
|
|
||||||
|
|
||||||
# Job Posting
|
# Job Posting
|
||||||
# def job_list(request):
|
# def job_list(request):
|
||||||
# """Display the list of job postings order by creation date descending"""
|
# """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):
|
def _handle_preview_submission(request, slug, job):
|
||||||
"""
|
"""
|
||||||
Handles the initial POST request (Preview Schedule).
|
Handles the initial POST request (Preview Schedule).
|
||||||
@ -1516,7 +1521,6 @@ def _handle_preview_submission(request, slug, job):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
# Get the form data
|
# Get the form data
|
||||||
applications = form.cleaned_data["applications"]
|
applications = form.cleaned_data["applications"]
|
||||||
interview_type = form.cleaned_data["interview_type"]
|
|
||||||
start_date = form.cleaned_data["start_date"]
|
start_date = form.cleaned_data["start_date"]
|
||||||
end_date = form.cleaned_data["end_date"]
|
end_date = form.cleaned_data["end_date"]
|
||||||
working_days = form.cleaned_data["working_days"]
|
working_days = form.cleaned_data["working_days"]
|
||||||
@ -1572,16 +1576,11 @@ def _handle_preview_submission(request, slug, job):
|
|||||||
for i, application in enumerate(applications):
|
for i, application in enumerate(applications):
|
||||||
slot = available_slots[i]
|
slot = available_slots[i]
|
||||||
preview_schedule.append(
|
preview_schedule.append(
|
||||||
{
|
{"application": application, "date": slot["date"], "time": slot["time"]}
|
||||||
"applications": applications,
|
|
||||||
"date": slot["date"],
|
|
||||||
"time": slot["time"],
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save the form data to session for later use
|
# Save the form data to session for later use
|
||||||
schedule_data = {
|
schedule_data = {
|
||||||
"interview_type": interview_type,
|
|
||||||
"start_date": start_date.isoformat(),
|
"start_date": start_date.isoformat(),
|
||||||
"end_date": end_date.isoformat(),
|
"end_date": end_date.isoformat(),
|
||||||
"working_days": working_days,
|
"working_days": working_days,
|
||||||
@ -1604,7 +1603,6 @@ def _handle_preview_submission(request, slug, job):
|
|||||||
{
|
{
|
||||||
"job": job,
|
"job": job,
|
||||||
"schedule": preview_schedule,
|
"schedule": preview_schedule,
|
||||||
"interview_type": interview_type,
|
|
||||||
"start_date": start_date,
|
"start_date": start_date,
|
||||||
"end_date": end_date,
|
"end_date": end_date,
|
||||||
"working_days": working_days,
|
"working_days": working_days,
|
||||||
@ -1842,13 +1840,12 @@ def _handle_confirm_schedule(request, slug, job):
|
|||||||
|
|
||||||
# 3. Setup candidates and get slots
|
# 3. Setup candidates and get slots
|
||||||
candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"])
|
candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"])
|
||||||
schedule.candidates.set(candidates)
|
schedule.applications.set(candidates)
|
||||||
available_slots = get_available_time_slots(
|
available_slots = get_available_time_slots(schedule)
|
||||||
schedule
|
|
||||||
) # This should still be synchronous and fast
|
|
||||||
|
|
||||||
# 4. Queue scheduled interviews asynchronously (FAST RESPONSE)
|
# 4. Handle Remote/Onsite logic
|
||||||
if schedule.interview_type == "Remote":
|
if schedule_data.get("schedule_interview_type") == 'Remote':
|
||||||
|
# ... (Remote logic remains unchanged)
|
||||||
queued_count = 0
|
queued_count = 0
|
||||||
for i, candidate in enumerate(candidates):
|
for i, candidate in enumerate(candidates):
|
||||||
if i < len(available_slots):
|
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]
|
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||||
|
|
||||||
return redirect("job_detail", slug=slug)
|
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):
|
def schedule_interviews_view(request, slug):
|
||||||
@ -2135,41 +2184,18 @@ def candidate_update_status(request, slug):
|
|||||||
def candidate_interview_view(request, slug):
|
def candidate_interview_view(request, slug):
|
||||||
job = get_object_or_404(JobPosting, slug=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 = {
|
context = {
|
||||||
"job": job,
|
"job": job,
|
||||||
"candidates": job.interview_candidates,
|
"candidates": job.interview_candidates,
|
||||||
"current_stage": "Interview",
|
"current_stage": "Interview",
|
||||||
"form": form,
|
|
||||||
"participants_count": 0 #job.participants.count() + job.users.count(),
|
|
||||||
}
|
}
|
||||||
return render(request, "recruitment/candidate_interview_view.html", context)
|
return render(request, "recruitment/candidate_interview_view.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def candidate_document_review_view(request, slug):
|
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
|
from .email_service import send_bulk_email
|
||||||
|
|
||||||
job = get_object_or_404(JobPosting, slug=job_slug)
|
job = get_object_or_404(JobPosting, slug=job_slug)
|
||||||
candidate = get_object_or_404(Application, slug=candidate_slug, job=job)
|
|
||||||
if request.method == "POST":
|
# # candidate = get_object_or_404(Application, slug=candidate_slug, job=job)
|
||||||
form = CandidateEmailForm(job, candidate, request.POST)
|
# if request.method == "POST":
|
||||||
candidate_ids = request.GET.getlist("candidate_ids")
|
# form = CandidateEmailForm(job, candidate, request.POST)
|
||||||
candidates = Application.objects.filter(id__in=candidate_ids)
|
candidate_ids=request.GET.getlist('candidate_ids')
|
||||||
|
candidates=Application.objects.filter(id__in=candidate_ids)
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
print(
|
if request.method == 'POST':
|
||||||
"........................................................inside candidate conpose............."
|
print("........................................................inside candidate conpose.............")
|
||||||
)
|
candidate_ids = request.POST.getlist('candidate_ids')
|
||||||
candidate_ids = request.POST.getlist("candidate_ids")
|
candidates=Application.objects.filter(id__in=candidate_ids)
|
||||||
candidates = Application.objects.filter(id__in=candidate_ids)
|
|
||||||
form = CandidateEmailForm(job, candidates, request.POST)
|
form = CandidateEmailForm(job, candidates, request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
print("form is valid ...")
|
print("form is valid ...")
|
||||||
# Get email addresses
|
# Get email addresses
|
||||||
email_addresses = form.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:
|
if not email_addresses:
|
||||||
messages.error(request, "No email selected")
|
messages.error(request, 'No email selected')
|
||||||
referer = request.META.get("HTTP_REFERER")
|
referer = request.META.get('HTTP_REFERER')
|
||||||
|
|
||||||
if referer:
|
if referer:
|
||||||
# Redirect back to the referring page
|
# Redirect back to the referring page
|
||||||
return redirect(referer)
|
return redirect(referer)
|
||||||
else:
|
else:
|
||||||
return redirect("dashboard")
|
|
||||||
|
return redirect('dashboard')
|
||||||
|
|
||||||
|
|
||||||
message = form.get_formatted_message()
|
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)
|
# 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,
|
request=request,
|
||||||
attachments=None,
|
attachments=None,
|
||||||
async_task_=True, # Changed to False to avoid pickle issues
|
async_task_=True, # Changed to False to avoid pickle issues
|
||||||
from_interview=False,
|
from_interview=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if email_result["success"]:
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
"includes/email_compose_form.html",
|
"includes/email_compose_form.html",
|
||||||
{"form": form, "job": job, "candidate": candidates},
|
{"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:
|
else:
|
||||||
# Form validation errors
|
# Form validation errors
|
||||||
print("form is not valid")
|
print('form is not valid')
|
||||||
print(form.errors)
|
print(form.errors)
|
||||||
messages.error(request, "Please correct the errors below.")
|
messages.error(request, "Please correct the errors below.")
|
||||||
|
|
||||||
@ -5484,9 +5455,8 @@ def compose_candidate_email(request, job_slug):
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"includes/email_compose_form.html",
|
"includes/email_compose_form.html",
|
||||||
{"form": form, "job": job, "candidates": candidate},
|
{"form": form, "job": job, "candidates": candidates},
|
||||||
s,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# GET request - show the form
|
# GET request - show the form
|
||||||
@ -5500,6 +5470,7 @@ def compose_candidate_email(request, job_slug):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Source CRUD Views
|
# Source CRUD Views
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def source_list(request):
|
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})
|
# return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule})
|
||||||
|
|
||||||
|
|
||||||
def onsite_interview_list_view(request):
|
class MeetingListView(ListView):
|
||||||
onsite_interviews = ScheduledInterview.objects.filter(
|
"""
|
||||||
schedule__interview_type="Onsite"
|
A unified view to list both Remote and Onsite Scheduled Interviews.
|
||||||
)
|
"""
|
||||||
return render(
|
model = ScheduledInterview
|
||||||
request,
|
template_name = "meetings/list_meetings.html"
|
||||||
"interviews/onsite_interview_list.html",
|
context_object_name = "meetings"
|
||||||
{"onsite_interviews": onsite_interviews},
|
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)
|
||||||
|
# =========================================================================
|
||||||
|
|||||||
@ -206,14 +206,11 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||||
|
|
||||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||||
<option selected>
|
<option selected>
|
||||||
----------
|
----------
|
||||||
</option>
|
</option>
|
||||||
<option value="Document Review">
|
|
||||||
{% trans "To Documents Review" %}
|
|
||||||
</option>
|
|
||||||
<option value="Offer">
|
<option value="Offer">
|
||||||
{% trans "To Offer" %}
|
{% trans "To Offer" %}
|
||||||
</option>
|
</option>
|
||||||
@ -236,7 +233,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-info btn-sm"
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -251,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||||
@ -285,6 +282,7 @@
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
@ -382,15 +380,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<<<<<<< HEAD
|
|
||||||
|
|
||||||
{% if candidate.get_latest_meeting %}
|
{% if candidate.get_latest_meeting %}
|
||||||
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
||||||
|
|
||||||
=======
|
|
||||||
|
|
||||||
{% if candidate.get_latest_meeting %}
|
|
||||||
>>>>>>> 1babb1be63436083b4a5ec7d76c115350b0c9f4a
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#candidateviewModal"
|
data-bs-target="#candidateviewModal"
|
||||||
@ -408,7 +401,6 @@
|
|||||||
title="Delete Meeting">
|
title="Delete Meeting">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
<<<<<<< HEAD
|
|
||||||
{% else%}
|
{% else%}
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
@ -431,9 +423,6 @@
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> 1babb1be63436083b4a5ec7d76c115350b0c9f4a
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-main-action btn-sm"
|
<button type="button" class="btn btn-main-action btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -508,7 +497,7 @@
|
|||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||||
{% trans "Loading email form..." %}
|
{% trans "Loading email form..." %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user