diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py new file mode 100644 index 0000000..009608e --- /dev/null +++ b/recruitment/migrations/0001_initial.py @@ -0,0 +1,754 @@ +# Generated by Django 6.0 on 2025-12-08 10:22 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import django_ckeditor_5.fields +import django_countries.fields +import django_extensions.db.fields +import recruitment.validators +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='BreakTime', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_time', models.TimeField(verbose_name='Start Time')), + ('end_time', models.TimeField(verbose_name='End Time')), + ], + ), + migrations.CreateModel( + name='EmailContent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(max_length=255, verbose_name='Subject')), + ('message', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Message Body')), + ], + options={ + 'verbose_name': 'Email Content', + 'verbose_name_plural': 'Email Contents', + }, + ), + migrations.CreateModel( + name='FormStage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(help_text='Name of the stage', max_length=200)), + ('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')), + ('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')), + ], + options={ + 'verbose_name': 'Form Stage', + 'verbose_name_plural': 'Form Stages', + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Interview', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')), + ('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')), + ('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)), + ('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')), + ('password', models.CharField(blank=True, max_length=20, null=True)), + ('zoom_gateway_response', models.JSONField(blank=True, null=True)), + ('details_url', models.JSONField(blank=True, null=True)), + ('participant_video', models.BooleanField(default=True)), + ('join_before_host', models.BooleanField(default=False)), + ('host_email', models.CharField(blank=True, max_length=255, null=True)), + ('mute_upon_entry', models.BooleanField(default=False)), + ('waiting_room', models.BooleanField(default=False)), + ('physical_address', models.CharField(blank=True, max_length=255, null=True)), + ('room_number', models.CharField(blank=True, max_length=50, null=True)), + ], + options={ + 'verbose_name': 'Interview Location', + 'verbose_name_plural': 'Interview Locations', + }, + ), + migrations.CreateModel( + name='Participants', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), + ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Settings', + 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')), + ('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')), + ], + options={ + 'verbose_name': 'Setting', + 'verbose_name_plural': 'Settings', + 'ordering': ['key'], + }, + ), + migrations.CreateModel( + name='Source', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')), + ('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')), + ('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')), + ('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')), + ('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')), + ('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')), + ('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')), + ('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')), + ('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')), + ('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('SUCCESS', 'Success'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')), + ('sync_endpoint', models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint')), + ('sync_method', models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method')), + ('test_method', models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method')), + ('custom_headers', models.JSONField(blank=True, default=dict, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers')), + ('supports_outbound_sync', models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync')), + ], + options={ + 'verbose_name': 'Source', + 'verbose_name_plural': 'Sources', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')), + ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), + ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), + ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), + ('email', models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'User', + 'verbose_name_plural': 'Users', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='FormField', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('label', models.CharField(help_text='Label for the field', max_length=200)), + ('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)), + ('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)), + ('required', models.BooleanField(default=False, help_text='Whether the field is required')), + ('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')), + ('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')), + ('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')), + ('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)), + ('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')), + ('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')), + ('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')), + ('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')), + ], + options={ + 'verbose_name': 'Form Field', + 'verbose_name_plural': 'Form Fields', + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='FormTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(help_text='Name of the form template', max_length=200)), + ('description', models.TextField(blank=True, help_text='Description of the form template')), + ('is_active', models.BooleanField(default=False, help_text='Whether this template is active')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Form Template', + 'verbose_name_plural': 'Form Templates', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='FormSubmission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), + ('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)), + ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')), + ], + options={ + 'verbose_name': 'Form Submission', + 'verbose_name_plural': 'Form Submissions', + 'ordering': ['-submitted_at'], + }, + ), + migrations.AddField( + model_name='formstage', + name='template', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'), + ), + migrations.CreateModel( + name='HiringAgency', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')), + ('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')), + ('email', models.EmailField(blank=True, max_length=254)), + ('phone', models.CharField(blank=True, max_length=20)), + ('website', models.URLField(blank=True)), + ('notes', models.TextField(blank=True, help_text='Internal notes about the agency')), + ('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)), + ('address', models.TextField(blank=True, null=True)), + ('generated_password', models.CharField(blank=True, help_text='Generated password for agency user account', max_length=255, null=True)), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agency_profile', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Hiring Agency', + 'verbose_name_plural': 'Hiring Agencies', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')), + ('cover_letter', models.FileField(blank=True, null=True, upload_to='cover_letters/', verbose_name='Cover Letter')), + ('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')), + ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), + ('applied', models.BooleanField(default=False, verbose_name='Applied')), + ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Document Review', 'Document Review'), ('Offer', 'Offer'), ('Hired', 'Hired'), ('Rejected', 'Rejected')], db_index=True, default='Applied', max_length=20, verbose_name='Stage')), + ('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=20, null=True, verbose_name='Applicant Status')), + ('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')), + ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Exam Status')), + ('exam_score', models.FloatField(blank=True, null=True, verbose_name='Exam Score')), + ('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')), + ('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Interview Status')), + ('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')), + ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected'), ('Pending', 'Pending')], max_length=20, null=True, verbose_name='Offer Status')), + ('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')), + ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), + ('ai_analysis_data', models.JSONField(blank=True, default=dict, help_text='Full JSON output from the resume scoring model.', null=True, verbose_name='AI Analysis Data')), + ('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')), + ('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')), + ('hiring_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ], + options={ + 'verbose_name': 'Application', + 'verbose_name_plural': 'Applications', + }, + ), + migrations.CreateModel( + name='JobPosting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('title', models.CharField(max_length=200)), + ('department', models.CharField(blank=True, max_length=100)), + ('job_type', models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='Full-time', max_length=20)), + ('workplace_type', models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20)), + ('location_city', models.CharField(blank=True, max_length=100)), + ('location_state', models.CharField(blank=True, max_length=100)), + ('location_country', models.CharField(default='Saudia Arabia', max_length=100)), + ('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')), + ('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), + ('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)), + ('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), + ('application_url', models.URLField(blank=True, help_text='URL where applicants apply', null=True, validators=[django.core.validators.URLValidator()])), + ('application_deadline', models.DateField(db_index=True)), + ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), + ('internal_job_id', models.CharField(editable=False, max_length=50)), + ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), + ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)), + ('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])), + ('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)), + ('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')), + ('posted_to_linkedin', models.BooleanField(default=False)), + ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), + ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), + ('linkedin_post_formated_data', models.TextField(blank=True, null=True)), + ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), + ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), + ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), + ('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')), + ('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)), + ('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')), + ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), + ('cancelled_at', models.DateTimeField(blank=True, null=True)), + ('ai_parsed', models.BooleanField(default=False, help_text='Whether the job posting has been parsed by AI', verbose_name='AI Parsed')), + ('cv_zip_file', models.FileField(blank=True, null=True, upload_to='job_zips/')), + ('zip_created', models.BooleanField(default=False)), + ('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 applicants for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')), + ], + options={ + 'verbose_name': 'Job Posting', + 'verbose_name_plural': 'Job Postings', + 'ordering': ['-created_at'], + }, + ), + migrations.AddField( + model_name='formtemplate', + name='job', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'), + ), + migrations.CreateModel( + name='BulkInterviewTemplate', + 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')), + ('start_date', models.DateField(db_index=True, verbose_name='Start Date')), + ('end_date', models.DateField(db_index=True, verbose_name='End Date')), + ('working_days', models.JSONField(verbose_name='Working Days')), + ('topic', models.CharField(max_length=255, verbose_name='Interview Topic')), + ('start_time', models.TimeField(verbose_name='Start Time')), + ('end_time', models.TimeField(verbose_name='End Time')), + ('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')), + ('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')), + ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), + ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), + ('schedule_interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type')), + ('physical_address', models.CharField(blank=True, max_length=255, null=True)), + ('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='application', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.jobposting', verbose_name='Job'), + ), + migrations.CreateModel( + name='AgencyJobAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')), + ('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')), + ('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')), + ('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')), + ('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')), + ('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')), + ('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')), + ('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')), + ], + options={ + 'verbose_name': 'Agency Job Assignment', + 'verbose_name_plural': 'Agency Job Assignments', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='JobPostingImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('post_image', models.ImageField(upload_to='post/', validators=[recruitment.validators.validate_image_size])), + ('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')), + ], + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('subject', models.CharField(max_length=200, verbose_name='Subject')), + ('content', models.TextField(verbose_name='Message Content')), + ('message_type', models.CharField(choices=[('direct', 'Direct Message'), ('job_related', 'Job Related'), ('system', 'System Notification')], default='direct', max_length=20, verbose_name='Message Type')), + ('is_read', models.BooleanField(default=False, verbose_name='Is Read')), + ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.jobposting', verbose_name='Related Job')), + ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), + ], + options={ + 'verbose_name': 'Message', + 'verbose_name_plural': 'Messages', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')), + ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.application', verbose_name='Application')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')), + ('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')), + ], + options={ + 'verbose_name': 'Interview Note', + 'verbose_name_plural': 'Interview Notes', + 'ordering': ['created_at'], + }, + ), + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField(verbose_name='Notification Message')), + ('notification_type', models.CharField(choices=[('email', 'Email'), ('in_app', 'In-App')], default='email', max_length=20, verbose_name='Notification Type')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('read', 'Read'), ('failed', 'Failed'), ('retrying', 'Retrying')], default='pending', max_length=20, verbose_name='Status')), + ('scheduled_for', models.DateTimeField(help_text='The date and time this notification is scheduled to be sent.', verbose_name='Scheduled Send Time')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('attempts', models.PositiveIntegerField(default=0, verbose_name='Send Attempts')), + ('last_error', models.TextField(blank=True, verbose_name='Last Error Message')), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Notification', + 'verbose_name_plural': 'Notifications', + 'ordering': ['-scheduled_for', '-created_at'], + }, + ), + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('first_name', models.CharField(max_length=255, verbose_name='First Name')), + ('last_name', models.CharField(max_length=255, verbose_name='Last Name')), + ('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')), + ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), + ('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')), + ('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')), + ('gpa', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='GPA')), + ('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')), + ('address', models.TextField(blank=True, null=True, verbose_name='Address')), + ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), + ('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')), + ('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')), + ], + options={ + 'verbose_name': 'Person', + 'verbose_name_plural': 'People', + }, + ), + migrations.AddField( + model_name='application', + name='person', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.person', verbose_name='Person'), + ), + migrations.CreateModel( + name='ScheduledInterview', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('interview_date', models.DateField(db_index=True, verbose_name='Interview Date')), + ('interview_time', models.TimeField(verbose_name='Interview Time')), + ('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)), + ('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')), + ('participants', models.ManyToManyField(blank=True, to='recruitment.participants')), + ('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interviews', to='recruitment.bulkinterviewtemplate')), + ('system_users', models.ManyToManyField(blank=True, related_name='attended_interviews', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='SharedFormTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')), + ('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)), + ('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')), + ], + options={ + 'verbose_name': 'Shared Form Template', + 'verbose_name_plural': 'Shared Form Templates', + }, + ), + migrations.CreateModel( + name='IntegrationLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')), + ('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')), + ('method', models.CharField(blank=True, max_length=50, verbose_name='HTTP Method')), + ('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')), + ('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')), + ('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')), + ('error_message', models.TextField(blank=True, verbose_name='Error Message')), + ('ip_address', models.GenericIPAddressField(verbose_name='IP Address')), + ('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')), + ('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')), + ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')), + ], + options={ + 'verbose_name': 'Integration Log', + 'verbose_name_plural': 'Integration Logs', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='AgencyAccessLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')), + ('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')), + ('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')), + ('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')), + ], + options={ + 'verbose_name': 'Agency Access Link', + 'verbose_name_plural': 'Agency Access Links', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx')], + }, + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('object_id', models.PositiveIntegerField(verbose_name='Object ID')), + ('file', models.FileField(upload_to='documents/%Y/%m/', validators=[recruitment.validators.validate_image_size], verbose_name='Document File')), + ('document_type', models.CharField(choices=[('resume', 'Resume'), ('cover_letter', 'Cover Letter'), ('certificate', 'Certificate'), ('id_document', 'ID Document'), ('passport', 'Passport'), ('education', 'Education Document'), ('experience', 'Experience Letter'), ('other', 'Other')], default='other', max_length=20, verbose_name='Document Type')), + ('description', models.CharField(blank=True, max_length=200, verbose_name='Description')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content Type')), + ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Uploaded By')), + ], + options={ + 'verbose_name': 'Document', + 'verbose_name_plural': 'Documents', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['content_type', 'object_id', 'document_type', 'created_at'], name='recruitment_content_547650_idx')], + }, + ), + migrations.CreateModel( + name='FieldResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)), + ('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')), + ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), + ], + options={ + 'verbose_name': 'Field Response', + 'verbose_name_plural': 'Field Responses', + 'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')], + }, + ), + migrations.AddIndex( + model_name='formsubmission', + index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), + ), + migrations.AddIndex( + model_name='formtemplate', + index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), + ), + migrations.AddIndex( + model_name='formtemplate', + index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'), + ), + migrations.AlterUniqueTogether( + name='agencyjobassignment', + unique_together={('agency', 'job')}, + ), + migrations.AddIndex( + model_name='message', + index=models.Index(fields=['sender', 'created_at'], name='recruitment_sender__49d984_idx'), + ), + migrations.AddIndex( + model_name='message', + index=models.Index(fields=['recipient', 'is_read', 'created_at'], name='recruitment_recipie_af0e6d_idx'), + ), + migrations.AddIndex( + model_name='message', + index=models.Index(fields=['job', 'created_at'], name='recruitment_job_id_18f813_idx'), + ), + migrations.AddIndex( + model_name='message', + index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'), + ), + migrations.AddIndex( + model_name='notification', + index=models.Index(fields=['status', 'scheduled_for'], name='recruitment_status_0ebbe4_idx'), + ), + migrations.AddIndex( + model_name='notification', + index=models.Index(fields=['recipient'], name='recruitment_recipie_eadf4c_idx'), + ), + migrations.AddIndex( + model_name='person', + index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'), + ), + migrations.AddIndex( + model_name='person', + index=models.Index(fields=['first_name', 'last_name'], name='recruitment_first_n_739de5_idx'), + ), + migrations.AddIndex( + model_name='person', + index=models.Index(fields=['created_at'], name='recruitment_created_33495a_idx'), + ), + migrations.AddIndex( + model_name='application', + index=models.Index(fields=['person', 'job'], name='recruitment_person__34355c_idx'), + ), + migrations.AddIndex( + model_name='application', + index=models.Index(fields=['stage'], name='recruitment_stage_52c2d1_idx'), + ), + migrations.AddIndex( + model_name='application', + index=models.Index(fields=['created_at'], name='recruitment_created_80633f_idx'), + ), + migrations.AddIndex( + model_name='application', + index=models.Index(fields=['person', 'stage', 'created_at'], name='recruitment_person__8715ec_idx'), + ), + migrations.AlterUniqueTogether( + name='application', + unique_together={('person', 'job')}, + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['application', 'job'], name='recruitment_applica_927561_idx'), + ), + migrations.AddIndex( + model_name='jobposting', + index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), + ), + migrations.AddIndex( + model_name='jobposting', + index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), + ), + ] diff --git a/recruitment/migrations/__init__.py b/recruitment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/recruitment/urls.py b/recruitment/urls.py index 5e641c5..73062c8 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -116,10 +116,10 @@ urlpatterns = [ # Application Forms (Public) path("application/signup//", views.application_signup, name="application_signup"), - path("application//", views.application_submit_form, name="application_submit_form"), - path("application//submit/", views.application_submit, name="application_submit"), - path("application//apply/", views.job_application_detail, name="job_application_detail"), - path("application//success/", views.application_success, name="application_success"), + path("application//", views.application_submit_form, name="application_submit_form"), + path("application//submit/", views.application_submit, name="application_submit"), + path("application//apply/", views.job_application_detail, name="job_application_detail"), + path("application//success/", views.application_success, name="application_success"), # ======================================================================== # INTEGRATION & EXTERNAL SERVICES diff --git a/recruitment/views.py b/recruitment/views.py index e6c8c60..f39f5ee 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -36,6 +36,8 @@ from django.db.models import ( ExpressionWrapper, IntegerField, ) +from django.utils.translation import get_language +from django.db.models import fields from django.db.models.fields.json import KeyTextTransform, KeyTransform from django.db.models.functions import Coalesce, Cast, TruncDate @@ -1015,11 +1017,11 @@ def delete_form_template(request, template_id): @login_required @staff_or_candidate_required -def application_submit_form(request, template_slug): +def application_submit_form(request, slug): """Display the form as a step-by-step wizard""" if not request.user.is_authenticated: - return redirect("application_signup",slug=template_slug) - job = get_object_or_404(JobPosting, form_template__slug=template_slug) + return redirect("application_signup",slug=slug) + job = get_object_or_404(JobPosting, form_template__slug=slug) if request.user.user_type == "candidate": person=request.user.person_profile if job.has_already_applied_to_this_job(person): @@ -1032,7 +1034,7 @@ def application_submit_form(request, template_slug): return redirect("job_application_detail", slug=job.slug) - template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True) + template = get_object_or_404(FormTemplate, slug=slug, is_active=True) stage = template.stages.filter(name="Contact Information") @@ -1059,7 +1061,7 @@ def application_submit_form(request, template_slug): return render( request, "applicant/application_submit_form.html", - {"template_slug": template_slug, "job_id": job_id}, + {"template_slug": slug, "job_id": job_id}, ) @csrf_exempt @@ -3704,7 +3706,7 @@ def interview_create_remote(request, application_slug): async_task("recruitment.tasks.create_interview_and_meeting",schedule.pk) messages.success(request, f"Remote interview scheduled for {application.name}") - return redirect('applications_interview_view', slug=application_slug) + return redirect('applications_interview_view', slug=application.job.slug) except Exception as e: messages.error(request, f"Error creating remote interview: {str(e)}") @@ -3739,7 +3741,7 @@ def interview_create_onsite(request, application_slug): schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) messages.success(request, f"Onsite interview scheduled for {application.name}") - return redirect('applications_interview_view', slug=application_slug) + return redirect('applications_interview_view', slug=application.job.slug) except Exception as e: messages.error(request, f"Error creating onsite interview: {str(e)}") @@ -4307,7 +4309,7 @@ def application_signup(request, slug): ) login(request, user,backend='django.contrib.auth.backends.ModelBackend') - return redirect("application_submit_form", template_slug=slug) + return redirect("application_submit_form", slug=slug) except Exception as e: messages.error(request, f"Error creating application: {str(e)}") return render( diff --git a/templates/applicant/career.html b/templates/applicant/career.html index ddaf7e4..bf664c0 100644 --- a/templates/applicant/career.html +++ b/templates/applicant/career.html @@ -3,17 +3,17 @@ {# Use a dynamic title for better SEO and user context #} {% block title %}{% trans "Career Opportunities" %} | KAAUH - {{ block.super }}{% endblock %} - + {% block content %}
- + {# ------------------------------------------------ #} {# 🌟 HERO SECTION (High Visual Impact) #} {# ------------------------------------------------ #}
{# Overlay for readability, assuming custom CSS defines .hero-overlay #} -
+
@@ -24,7 +24,7 @@

{% trans "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}

- + {# Primary Action to scroll to listings/filters #} {% trans "Find Your Path" %} @@ -37,21 +37,21 @@
- - + + {# ------------------------------------------------ #} {# 💻 JOB LISTING SECTION #} {# ------------------------------------------------ #}
- +
- + {# 📌 LEFT COLUMN: FILTERS (Smaller on larger screens) #}
- + {# Mobile Filter Toggle (Used aria-controls for better accessibility) #} - @@ -59,18 +59,18 @@
{# Sticky top ensures filters remain visible while scrolling results #}
- +

{% trans "Refine Your Search" %}

{# NOTE: Replace select with Django form fields for real functionality #} - - + + - + {% for key in workplace_type_keys %} {% endfor %} @@ -92,16 +92,16 @@ + @@ -109,25 +109,25 @@
- - + +
{# 📜 RIGHT COLUMN: JOB LISTINGS (Wider on larger screens) #}
- +
{% endblock content %} \ No newline at end of file