diff --git a/.env b/.env index b9e2bf0..8d7fbd5 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -DB_NAME=norahuniversity -DB_USER=norahuniversity -DB_PASSWORD=norahuniversity \ No newline at end of file +DB_NAME=haikal_db +DB_USER=faheed +DB_PASSWORD=Faheed@215 \ No newline at end of file diff --git a/recruitment/forms.py b/recruitment/forms.py index 1263e86..b58f18c 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -359,6 +359,10 @@ class ApplicationForm(forms.ModelForm): self.fields['job'].initial = current_job self.fields['job'].widget.attrs['readonly'] = True + else: + self.fields['job'].queryset = self.fields['job'].queryset.filter( + status="ACTIVE" + ) # Make job field read-only if it's being pre-populated job_value = self.initial.get("job") @@ -706,7 +710,7 @@ class BulkInterviewTemplateForm(forms.ModelForm): def clean_start_date(self): start_date = self.cleaned_data.get("start_date") - if start_date and start_date <= timezone.now().date(): + if start_date and start_date < timezone.now().date(): raise forms.ValidationError(_("Start date must be in the future")) return start_date @@ -1389,43 +1393,70 @@ class CandidateEmailForm(forms.Form): if candidate and candidate.stage == 'Applied': message_parts = [ - f"Than you, for your interest in the {self.job.title} role.", - f"We regret to inform you that you were not selected to move forward to the exam round at this time.", - f"We encourage you to check our career page for further updates and future opportunities:", - f"https://kaauh/careers", - f"Wishing you the best in your job search,", - f"The KAAUH Hiring team" + f"Thank you for your interest in the {self.job.title} position at KAAUH and for taking the time to submit your application.", + f"We have carefully reviewed your qualifications; however, we regret to inform you that your application was not selected to proceed to the examination round at this time.", + f"The selection process was highly competitive, and we had a large number of highly qualified applicants.", + f"We encourage you to review other opportunities and apply for roles that align with your skills on our career portal:", + f"[settings.CAREER_PAGE_URL]", # Use a Django setting for the URL for flexibility + f"We wish you the best of luck in your current job search and future career endeavors.", + f"Sincerely,", + f"The KAAUH Recruitment Team", ] elif candidate and candidate.stage == 'Exam': message_parts = [ - f"Than you,for your interest in the {self.job.title} role.", - f"We're pleased to inform you that your initial screening was successful!", - f"The next step is the mandatory online assessment exam.", - f"Please complete the assessment by using the following link:", - f"https://kaauh/hire/exam", - f"We look forward to reviewing your results.", - f"Best regards, The KAAUH Hiring team" + f"Dear Candidate,", + f"Thank you once again for your continued interest in the **{self.job.title}** position.", + f"We are pleased to inform you that, following a careful review of your application, you have been **selected to proceed to the next phase** of our recruitment process.", + f"The next mandatory step is the **Online Assessment Examination** designed to evaluate essential skills for this role.", + f"\n**Action Required:**", + f"Please click on the link below to access and complete the assessment:", + f"[settings.EXAM_LINK_URL]", # Using a settings variable is a professional best practice + f"\n**Important Details:**", + f"* **Deadline:** The exam must be completed within **72 hours** of receiving this notification.", + f"* **Duration:** The assessment is timed and will take approximately [Insert Time e.g., 60 minutes] to complete.", + f"* **Technical Note:** Please ensure you have a stable internet connection before beginning.", + f"We appreciate your dedication to this process and look forward to reviewing your results.", + f"Sincerely,", + f"The KAAUH Recruitment Team", ] - elif candidate and candidate.stage == 'Interview': + elif candidate and candidate.stage == 'Interview': message_parts = [ - f"Than you, for your interest in the {self.job.title} role.", - f"We're pleased to inform you that you have cleared your exam!", - f"The next step is the mandatory interview.", - f"Please complete the assessment by using the following link:", - f"https://kaauh/hire/exam", - f"We look forward to reviewing your results.", - f"Best regards, The KAAUH Hiring team" + f"Dear Candidate,", + f"Thank you for your performance in the recent assessment for the **{self.job.title}** role.", + f"We are pleased to inform you that you have **successfully cleared the examination phase** and have been selected to proceed to an interview.", + f"The interview is a mandatory step that allows us to learn more about your experience and fit for the role.", + f"\n**Next Steps:**", + f"Our recruitment coordinator will contact you directly within the next 1-2 business days to schedule your interview time and provide the necessary details (such as the interview panel, format, and location/virtual meeting link).", + f"\n**Please ensure your phone number and email address are current.**", + f"We look forward to speaking with you and discussing this exciting opportunity further.", + f"Sincerely,", + f"The KAAUH Recruitment Team", ] - elif candidate and candidate.stage == 'Offer': + elif candidate and candidate.stage == 'Offer': + message_parts = [ + f"Dear Candidate,", + f"We are delighted to extend to you a **formal offer of employment** for the position of **{self.job.title}** at KAAUH.", + f"Congratulations! This is an exciting moment, and we are very enthusiastic about the prospect of you joining our team.", + f"\n**Next Steps & Documentation:**", + f"A comprehensive offer package, detailing your compensation, benefits, and the full terms of employment, will be transmitted to your email address within the next **24 hours**.", + f"Please review this document carefully.", + f"\n**Questions and Support:**", + f"Should you have any immediate questions regarding the offer or the next steps, please do not hesitate to contact our Human Resources department directly at [HR Contact Email/Phone].", + f"\nWe eagerly anticipate your favorable response and officially welcoming you to the KAAUH team!", + f"Sincerely,", + f"The KAAUH Recruitment Team", + ] + elif candidate and candidate.stage == 'Document Review': message_parts = [ - f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.", - f"This is an exciting moment, and we look forward to having you join the KAAUH team.", - f"A detailed offer letter and compensation package will be sent to you via email within 24 hours.", - f"In the meantime, please contact our HR department at [HR Contact] if you have immediate questions.", - f"Welcome to the team!", - f"Best regards, The KAAUH Hiring team" + f"Congratulations on progressing to the final stage for the {self.job.title} role!", + f"The next critical step is to complete your application by uploading the required employment verification documents.", + f"**Please log into the Candidate Portal immediately** to access the 'Document Upload' section.", + f"Required documents typically include: National ID/Iqama, Academic Transcripts, and Professional Certifications.", + f"You have **7 days** to upload all documents. Failure to do so may delay or invalidate your candidacy.", + f"If you encounter any technical issues, please contact our support team at [Support Email/Phone] immediately.", + f"We appreciate your cooperation as we finalize your employment process.", ] elif candidate and candidate.stage == 'Hired': message_parts = [ @@ -1434,7 +1465,7 @@ class CandidateEmailForm(forms.Form): f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.", f"We look forward to seeing you at KAAUH.", f"If you have any questions before your start date, please contact [Onboarding Contact].", - f"Best regards, The KAAUH Hiring team" + ] elif candidate: message_parts="" @@ -2171,14 +2202,11 @@ Job: {job.title} """ if interview.location_type == 'Remote': - initial_message += f"Pease join using meeting link {interview.details_url} .\n\n" + initial_message += f"Pease join using meeting link {interview.join_url} \n\n" else: initial_message += "This is an onsite schedule. Please arrive 10 minutes early.\n\n" - initial_message += """ -Best regards, -KAAUH Hiring Team - """ + self.fields['message'].initial = initial_message diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 2aaf4db..e4dfd2a 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0 on 2025-12-12 11:17 +# Generated by Django 5.2.7 on 2025-12-16 14:20 import django.contrib.auth.models import django.contrib.auth.validators @@ -92,11 +92,14 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')), + ('interview_result', models.CharField(blank=True, choices=[('passed', 'Passed'), ('failed', 'Failed'), ('on_hold', 'ON Hold')], default='on_hold', max_length=10, null=True, verbose_name='Interview Result')), + ('result_comments', models.TextField(blank=True, null=True)), ('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')), + ('join_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')), ('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')), ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), ('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), - ('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)), + ('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('deleted', 'Deleted'), ('ended', 'Ended')], db_index=True, default='waiting', max_length=20)), ('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='Cancelled At')), ('cancelled_reason', models.TextField(blank=True, null=True, verbose_name='Cancellation Reason')), ('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')), @@ -125,7 +128,7 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')), ('email', models.EmailField(max_length=254, verbose_name='Email')), - ('phone', secured_fields.fields.EncryptedCharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), + ('phone', secured_fields.fields.EncryptedCharField(blank=True, max_length=12, null=True, searchable=True, verbose_name='Phone Number')), ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), ], options={ @@ -139,8 +142,9 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(blank=True, help_text="A human-readable name (e.g., 'Zoom')", max_length=100, null=True, verbose_name='Friendly Name')), ('key', models.CharField(help_text='Unique key for the setting', max_length=100, unique=True, verbose_name='Setting Key')), - ('value', models.TextField(help_text='Value for the setting', verbose_name='Setting Value')), + ('value', secured_fields.fields.EncryptedTextField(help_text='Value for the setting', verbose_name='Setting Value')), ], options={ 'verbose_name': 'Setting', @@ -154,8 +158,8 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')), - ('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')), + ('name', models.CharField(help_text='Name of the source', max_length=100, unique=True, verbose_name='Source Name')), + ('source_type', models.CharField(help_text='Type of the source', max_length=100, verbose_name='Source Type')), ('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')), ('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')), ('created_at', models.DateTimeField(auto_now_add=True)), @@ -190,9 +194,9 @@ class Migration(migrations.Migration): ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('first_name', secured_fields.fields.EncryptedCharField(blank=True, max_length=150, verbose_name='first name')), + ('first_name', secured_fields.fields.EncryptedCharField(blank=True, max_length=150, searchable=True, verbose_name='first name')), ('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], db_index=True, default='staff', max_length=20, verbose_name='User Type')), - ('phone', secured_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone')), + ('phone', secured_fields.fields.EncryptedCharField(blank=True, null=True, searchable=True, verbose_name='Phone')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), ('email', models.EmailField(db_index=True, error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)), @@ -358,7 +362,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')), ('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')), ('email', models.EmailField(max_length=254, unique=True)), - ('phone', secured_fields.fields.EncryptedCharField(blank=True, max_length=20, null=True)), + ('phone', secured_fields.fields.EncryptedCharField(blank=True, max_length=20, null=True, searchable=True)), ('website', models.URLField(blank=True)), ('notes', models.TextField(blank=True, help_text='Internal notes about the agency')), ('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)), @@ -581,11 +585,11 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('first_name', secured_fields.fields.EncryptedCharField(max_length=255, verbose_name='First Name')), + ('first_name', secured_fields.fields.EncryptedCharField(max_length=255, searchable=True, verbose_name='First Name')), ('last_name', models.CharField(max_length=255, verbose_name='Last Name')), ('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')), ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')), - ('phone', secured_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone')), + ('phone', secured_fields.fields.EncryptedCharField(blank=True, null=True, searchable=True, verbose_name='Phone')), ('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')), ('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')), ('gpa', models.DecimalField(decimal_places=2, help_text='GPA must be between 0 and 4.', max_digits=3, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(4)], verbose_name='GPA')), @@ -620,6 +624,7 @@ class Migration(migrations.Migration): ('interview_time', models.TimeField(verbose_name='Interview Time')), ('interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], default='Remote', max_length=20)), ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)), + ('interview_questions', models.JSONField(blank=True, null=True, verbose_name='Question Data')), ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.application')), ('interview', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interview', verbose_name='Interview/Meeting')), ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')), diff --git a/recruitment/migrations/0002_alter_source_name_alter_source_source_type.py b/recruitment/migrations/0002_alter_source_name_alter_source_source_type.py deleted file mode 100644 index 3b4ce6a..0000000 --- a/recruitment/migrations/0002_alter_source_name_alter_source_source_type.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0 on 2025-12-12 11:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='source', - name='name', - field=models.CharField(help_text='Name of the source', max_length=100, unique=True, verbose_name='Source Name'), - ), - migrations.AlterField( - model_name='source', - name='source_type', - field=models.CharField(help_text='Type of the source', max_length=100, verbose_name='Source Type'), - ), - ] diff --git a/recruitment/migrations/0003_interview_interview_result_interview_result_comments.py b/recruitment/migrations/0003_interview_interview_result_interview_result_comments.py deleted file mode 100644 index 738e042..0000000 --- a/recruitment/migrations/0003_interview_interview_result_interview_result_comments.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-12-15 12:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_alter_source_name_alter_source_source_type'), - ] - - operations = [ - migrations.AddField( - model_name='interview', - name='interview_result', - field=models.CharField(blank=True, choices=[('passed', 'Passed'), ('failed', 'Failed'), ('on_hold', 'ON Hold')], default='on_hold', max_length=10, null=True, verbose_name='Interview Result'), - ), - migrations.AddField( - model_name='interview', - name='result_comments', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0004_interviewquestion.py b/recruitment/migrations/0004_interviewquestion.py deleted file mode 100644 index 17b0c18..0000000 --- a/recruitment/migrations/0004_interviewquestion.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 6.0 on 2025-12-15 13:59 - -import django.db.models.deletion -import django_extensions.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_interview_interview_result_interview_result_comments'), - ] - - operations = [ - migrations.CreateModel( - name='InterviewQuestion', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('question_text', models.TextField(verbose_name='Question Text')), - ('question_type', models.CharField(choices=[('technical', 'Technical'), ('behavioral', 'Behavioral'), ('situational', 'Situational')], default='technical', max_length=20, verbose_name='Question Type')), - ('difficulty_level', models.CharField(choices=[('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard')], default='medium', max_length=20, verbose_name='Difficulty Level')), - ('category', models.CharField(blank=True, max_length=100, verbose_name='Category')), - ('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ai_questions', to='recruitment.scheduledinterview', verbose_name='Interview Schedule')), - ], - options={ - 'verbose_name': 'Interview Question', - 'verbose_name_plural': 'Interview Questions', - 'ordering': ['created_at'], - 'indexes': [models.Index(fields=['schedule', 'question_type'], name='recruitment_schedul_b09a70_idx')], - }, - ), - ] diff --git a/recruitment/migrations/0004_settings_name.py b/recruitment/migrations/0004_settings_name.py deleted file mode 100644 index c5c7f1f..0000000 --- a/recruitment/migrations/0004_settings_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-12-15 14:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_interview_interview_result_interview_result_comments'), - ] - - operations = [ - migrations.AddField( - model_name='settings', - name='name', - field=models.CharField(blank=True, help_text="A human-readable name (e.g., 'Zoom')", max_length=100, null=True, verbose_name='Friendly Name'), - ), - ] diff --git a/recruitment/migrations/0005_merge_0004_interviewquestion_0004_settings_name.py b/recruitment/migrations/0005_merge_0004_interviewquestion_0004_settings_name.py deleted file mode 100644 index 1ba0c59..0000000 --- a/recruitment/migrations/0005_merge_0004_interviewquestion_0004_settings_name.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 6.0 on 2025-12-16 08:41 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_interviewquestion'), - ('recruitment', '0004_settings_name'), - ] - - operations = [ - ] diff --git a/recruitment/migrations/0006_interview_join_url.py b/recruitment/migrations/0006_interview_join_url.py deleted file mode 100644 index b7641f2..0000000 --- a/recruitment/migrations/0006_interview_join_url.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0 on 2025-12-16 09:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_merge_0004_interviewquestion_0004_settings_name'), - ] - - operations = [ - migrations.AddField( - model_name='interview', - name='join_url', - field=models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL'), - ), - ] diff --git a/recruitment/migrations/0007_alter_interview_status.py b/recruitment/migrations/0007_alter_interview_status.py deleted file mode 100644 index 3951a2f..0000000 --- a/recruitment/migrations/0007_alter_interview_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0 on 2025-12-16 10:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_interview_join_url'), - ] - - operations = [ - migrations.AlterField( - model_name='interview', - name='status', - field=models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('ended', 'Ended'), ('deleted', 'Deleted')], db_index=True, default='waiting', max_length=20), - ), - ] diff --git a/recruitment/migrations/0008_remove_interviewquestion_recruitment_schedul_b09a70_idx_and_more.py b/recruitment/migrations/0008_remove_interviewquestion_recruitment_schedul_b09a70_idx_and_more.py deleted file mode 100644 index 906a5c0..0000000 --- a/recruitment/migrations/0008_remove_interviewquestion_recruitment_schedul_b09a70_idx_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 6.0 on 2025-12-16 10:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_alter_interview_status'), - ] - - operations = [ - migrations.RemoveIndex( - model_name='interviewquestion', - name='recruitment_schedul_b09a70_idx', - ), - migrations.AddField( - model_name='interviewquestion', - name='data', - field=models.JSONField(blank=True, default=1, verbose_name='Question Data'), - preserve_default=False, - ), - migrations.AlterField( - model_name='interview', - name='status', - field=models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('deleted', 'Deleted'), ('ended', 'Ended')], db_index=True, default='waiting', max_length=20), - ), - migrations.AddIndex( - model_name='interviewquestion', - index=models.Index(fields=['schedule'], name='recruitment_schedul_dbb350_idx'), - ), - migrations.RemoveField( - model_name='interviewquestion', - name='category', - ), - migrations.RemoveField( - model_name='interviewquestion', - name='difficulty_level', - ), - migrations.RemoveField( - model_name='interviewquestion', - name='question_text', - ), - migrations.RemoveField( - model_name='interviewquestion', - name='question_type', - ), - ] diff --git a/recruitment/migrations/0009_remove_interviewquestion_recruitment_schedul_dbb350_idx_and_more.py b/recruitment/migrations/0009_remove_interviewquestion_recruitment_schedul_dbb350_idx_and_more.py deleted file mode 100644 index 291e5be..0000000 --- a/recruitment/migrations/0009_remove_interviewquestion_recruitment_schedul_dbb350_idx_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 6.0 on 2025-12-16 10:48 - -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_remove_interviewquestion_recruitment_schedul_b09a70_idx_and_more'), - ] - - operations = [ - migrations.RemoveIndex( - model_name='interviewquestion', - name='recruitment_schedul_dbb350_idx', - ), - migrations.AddField( - model_name='scheduledinterview', - name='interview_questions', - field=models.JSONField(blank=True, default={}, verbose_name='Question Data'), - preserve_default=False, - ), - migrations.DeleteModel( - name='InterviewQuestion', - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index af5a137..f12c1b7 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -17,7 +17,7 @@ from django_countries.fields import CountryField from django_ckeditor_5.fields import CKEditor5Field from django_extensions.db.fields import RandomCharField from django.contrib.postgres.validators import MinValueValidator, MaxValueValidator -from secured_fields import EncryptedCharField +from secured_fields import EncryptedCharField,EncryptedTextField from typing import List, Dict, Any @@ -45,7 +45,7 @@ class CustomUser(AbstractUser): ("candidate", _("Candidate")), ] - first_name=EncryptedCharField(_("first name"), max_length=150, blank=True) + first_name=EncryptedCharField(_("first name"), max_length=150, blank=True,searchable=True) user_type = models.CharField( max_length=20, @@ -55,7 +55,7 @@ class CustomUser(AbstractUser): db_index=True, # Added index for user_type filtering ) phone = EncryptedCharField( - blank=True, null=True, verbose_name=_("Phone") + blank=True, null=True, verbose_name=_("Phone"),searchable=True ) profile_image = models.ImageField( null=True, @@ -535,7 +535,7 @@ class Person(Base): ] # Personal Information - first_name = EncryptedCharField(max_length=255, verbose_name=_("First Name")) + first_name = EncryptedCharField(max_length=255, verbose_name=_("First Name"),searchable=True) last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) middle_name = models.CharField( max_length=255, blank=True, null=True, verbose_name=_("Middle Name") @@ -546,7 +546,7 @@ class Person(Base): verbose_name=_("Email"), ) phone = EncryptedCharField( - blank=True, null=True, verbose_name=_("Phone") + blank=True, null=True, verbose_name=_("Phone"),searchable=True ) date_of_birth = models.DateField( null=True, blank=True, verbose_name=_("Date of Birth") @@ -1354,7 +1354,7 @@ class ScheduledInterview(Base): ) interview_questions = models.JSONField( verbose_name=_("Question Data"), - blank=True + blank=True,null=True ) def __str__(self): @@ -1968,7 +1968,7 @@ class HiringAgency(Base): max_length=150, blank=True, verbose_name=_("Contact Person") ) email = models.EmailField(unique=True) - phone = EncryptedCharField(max_length=20, blank=True,null=True) + phone = EncryptedCharField(max_length=20, blank=True,null=True,searchable=True) website = models.URLField(blank=True) notes = models.TextField(blank=True, help_text=_("Internal notes about the agency")) country = CountryField(blank=True, null=True, blank_label=_("Select country")) @@ -2362,7 +2362,7 @@ class Participants(Base): ) email =models.EmailField(verbose_name=_("Email")) phone = EncryptedCharField( - max_length=12, verbose_name=_("Phone Number"), null=True, blank=True + max_length=12, verbose_name=_("Phone Number"), null=True, blank=True,searchable=True ) designation = models.CharField( max_length=100, blank=True, verbose_name=_("Designation"), null=True @@ -2611,7 +2611,7 @@ class Settings(Base): verbose_name=_("Setting Key"), help_text=_("Unique key for the setting"), ) - value = models.TextField( + value = EncryptedTextField( verbose_name=_("Setting Value"), help_text=_("Value for the setting"), ) diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 243300c..0d47fea 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -27,6 +27,8 @@ from django.template.loader import render_to_string from .models import BulkInterviewTemplate, Interview, Message, ScheduledInterview from django.contrib.auth import get_user_model from .utils import get_setting +from pypdf import PdfReader + User = get_user_model() # Add python-docx import for Word document processing @@ -227,6 +229,10 @@ def format_job_description(pk): def ai_handler(prompt): print("model call") + OPENROUTER_API_URL = get_setting("OPENROUTER_API_URL") + OPENROUTER_API_KEY = get_setting("OPENROUTER_API_KEY") + OPENROUTER_MODEL = get_setting("OPENROUTER_MODEL") + print(OPENROUTER_MODEL) response = requests.post( url=OPENROUTER_API_URL, headers={ diff --git a/recruitment/views.py b/recruitment/views.py index 659aab0..5fce1f0 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -184,6 +184,7 @@ class PersonListView(StaffRequiredMixin, ListView, LoginRequiredMixin): model = Person template_name = "people/person_list.html" context_object_name = "people_list" + paginate_by=100 def get_queryset(self): queryset = super().get_queryset().select_related("user") @@ -2529,13 +2530,14 @@ def agency_list(request): | Q(contact_person__icontains=search_query) | Q(email__icontains=search_query) | Q(country__icontains=search_query) + | Q(phone=search_query) ) # Order by most recently created agencies = agencies.order_by("-created_at") # Pagination - paginator = Paginator(agencies, 10) # Show 10 agencies per page + paginator = Paginator(agencies,20) # Show 10 agencies per page page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) @@ -4474,7 +4476,7 @@ def source_list(request): """List all sources with search and pagination""" search_query = request.GET.get("q", "") sources = Source.objects.all() - + if search_query: sources = sources.filter( Q(name__icontains=search_query) @@ -4486,7 +4488,7 @@ def source_list(request): sources = sources.order_by("-created_at") # Pagination - paginator = Paginator(sources, 15) # Show 15 sources per page + paginator = Paginator(sources, 1) # Show 15 sources per page page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) @@ -4761,7 +4763,7 @@ def interview_list(request): "status_filter": status_filter, "job_filter": job_filter, "search_query": search_query, - "interviews": interviews, + "interviews": page_obj, "jobs": jobs, } return render(request, "interviews/interview_list.html", context) diff --git a/recruitment/views_source.py b/recruitment/views_source.py index c1e2a3e..96b0be3 100644 --- a/recruitment/views_source.py +++ b/recruitment/views_source.py @@ -23,7 +23,7 @@ class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView): queryset = super().get_queryset().order_by('name') # Search functionality - search_query = self.request.GET.get('search', '') + search_query = self.request.GET.get('q', '') if search_query: queryset = queryset.filter( models.Q(name__icontains=search_query) | diff --git a/templates/interviews/interview_list.html b/templates/interviews/interview_list.html index 04c1136..7837ef9 100644 --- a/templates/interviews/interview_list.html +++ b/templates/interviews/interview_list.html @@ -15,6 +15,7 @@ --kaauh-info: #17a2b8; --kaauh-danger: #dc3545; --kaauh-warning: #ffc107; + --kaauh-gray-light: #f8f9fa; /* Added for consistency */ } /* Primary Color Overrides */ @@ -93,14 +94,12 @@ } /* Column Widths */ - .interview-table thead th:nth-child(1) { width: 40px; } + .interview-table thead th:nth-child(1) { width: 18%; } .interview-table thead th:nth-child(2) { width: 15%; } - .interview-table thead th:nth-child(3) { width: 12%; } - .interview-table thead th:nth-child(4) { width: 12%; } + .interview-table thead th:nth-child(3) { width: 15%; } + .interview-table thead th:nth-child(4) { width: 10%; } .interview-table thead th:nth-child(5) { width: 10%; } - .interview-table thead th:nth-child(6) { width: 8%; } - .interview-table thead th:nth-child(7) { width: 8%; } - .interview-table thead th:nth-child(8) { width: 15%; } + .interview-table thead th:nth-child(6) { width: 10%; } /* Candidate and Job Info */ .candidate-name { @@ -130,14 +129,6 @@ font-weight: 600; } - /* Status Colors */ - .bg-scheduled { background-color: #6c757d !important; color: white; } - .bg-confirmed { background-color: var(--kaauh-info) !important; color: white; } - .bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; } - .bg-completed { background-color: var(--kaauh-success) !important; color: white; } - .bg-remote { background-color: #007bff !important; color: white; } - .bg-onsite { background-color: #6f42c1 !important; color: white; } - /* Custom Height Optimization */ .form-control-sm, .btn-sm { @@ -165,7 +156,6 @@ {% block content %}
-

@@ -173,7 +163,8 @@ {% trans "Interview Management" %}

- {% trans "Total Interviews:" %} {{ interviews|length }} + {# Using count() instead of length filter if interviews is the Paginator Page Object #} + {% trans "Total Interviews:" %} {{ interviews.paginator.count }}

@@ -183,19 +174,9 @@
-
- {% comment %} - {% endcomment %} -
-
+
{# Pagination (Standardized to Reference) #} - {% include "includes/paginator.html" %} + {% include "includes/paginator.html" %} {% else %}
diff --git a/templates/recruitment/source_list.html b/templates/recruitment/source_list.html index 1292a56..bdf8d9b 100644 --- a/templates/recruitment/source_list.html +++ b/templates/recruitment/source_list.html @@ -128,49 +128,8 @@
- {% if page_obj.has_other_pages %} -
- {% endif %} + + {% include "includes/paginator.html" %} {% else %}