diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index ec10c53..62636ce 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index ebf0204..4b04a43 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index 71ee1e2..fcfcc91 100644 Binary files a/recruitment/__pycache__/signals.cpython-313.pyc and b/recruitment/__pycache__/signals.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index 3d7de95..11edbba 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-313.pyc b/recruitment/__pycache__/utils.cpython-313.pyc index b60ea1b..4e08a67 100644 Binary files a/recruitment/__pycache__/utils.cpython-313.pyc and b/recruitment/__pycache__/utils.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 708fe60..2121db6 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 866307e..3363fb2 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -1,24 +1,24 @@ from django import forms from .validators import validate_hash_tags from django.core.validators import URLValidator +from django.forms.formsets import formset_factory from django.utils.translation import gettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div -from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule +from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule,BreakTime from django_summernote.widgets import SummernoteWidget class CandidateForm(forms.ModelForm): class Meta: model = Candidate - fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume', 'stage'] + fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume',] labels = { 'first_name': _('First Name'), 'last_name': _('Last Name'), 'phone': _('Phone'), 'email': _('Email'), 'resume': _('Resume'), - 'stage': _('Application Stage'), } widgets = { 'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter first name')}), @@ -154,17 +154,17 @@ class TrainingMaterialForm(forms.ModelForm): widgets = { 'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter material title')}), # 💡 Use SummernoteWidget here - 'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}), + 'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}), 'video_link': forms.URLInput(attrs={'class': 'form-control', 'placeholder': _('https://www.youtube.com/watch?v=...')}), 'file': forms.FileInput(attrs={'class': 'form-control'}), } - + # The __init__ and FormHelper layout remains the same def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_method = 'post' - self.helper.form_class = 'g-3' + self.helper.form_class = 'g-3' self.helper.layout = Layout( 'title', @@ -175,7 +175,7 @@ class TrainingMaterialForm(forms.ModelForm): css_class='g-3 mb-4' ), Div( - Submit('submit', _('Create Material'), + Submit('submit', _('Create Material'), css_class='btn btn-main-action'), css_class='col-12 mt-4' ) @@ -261,7 +261,7 @@ class JobPostingForm(forms.ModelForm): 'application_instructions': SummernoteWidget(attrs={ 'placeholder': 'Special instructions for applicants (e.g., required documents, reference requirements, etc.)', - + }), 'open_positions': forms.NumberInput(attrs={ 'class': 'form-control', @@ -412,11 +412,21 @@ class FormTemplateForm(forms.ModelForm): Field('is_active', css_class='form-check-input'), Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3') ) +class BreakTimeForm(forms.ModelForm): + class Meta: + model = BreakTime + fields = ['start_time', 'end_time'] + widgets = { + 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + } + +BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True) class InterviewScheduleForm(forms.ModelForm): candidates = forms.ModelMultipleChoiceField( queryset=Candidate.objects.none(), - widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}), + widget=forms.CheckboxSelectMultiple, required=True ) working_days = forms.MultipleChoiceField( @@ -429,24 +439,23 @@ class InterviewScheduleForm(forms.ModelForm): (5, 'Saturday'), (6, 'Sunday'), ], - widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}), + widget=forms.CheckboxSelectMultiple, required=True -) + ) class Meta: model = InterviewSchedule fields = [ 'candidates', 'start_date', 'end_date', 'working_days', - 'start_time', 'end_time', 'break_start_time', 'break_end_time', - 'interview_duration', 'buffer_time' + 'start_time', 'end_time', 'interview_duration', 'buffer_time' ] widgets = { 'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), - 'break_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), - 'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}), + 'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}), } def __init__(self, slug, *args, **kwargs): @@ -456,11 +465,7 @@ class InterviewScheduleForm(forms.ModelForm): job__slug=slug, stage='Interview' ) - self.helper = FormHelper() - self.helper.form_method = 'post' - self.helper.form_class = 'form-horizontal' - self.helper.label_class = 'col-md-3' - self.helper.field_class = 'col-md-9' + def clean_working_days(self): working_days = self.cleaned_data.get('working_days') # Convert string values to integers diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index fc8a860..d0381aa 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,6 +1,11 @@ -# Generated by Django 5.2.1 on 2025-05-18 17:23 +# Generated by Django 5.2.6 on 2025-10-08 15:48 +import django.core.validators import django.db.models.deletion +import django_countries.fields +import django_extensions.db.fields +import recruitment.validators +from django.conf import settings from django.db import migrations, models @@ -9,32 +14,375 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Job', + name='BreakTime', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('description_en', models.TextField()), - ('description_ar', models.TextField()), - ('is_published', models.BooleanField(default=False)), - ('posted_to_linkedin', models.BooleanField(default=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), + ('start_time', models.TimeField(verbose_name='Start Time')), + ('end_time', models.TimeField(verbose_name='End Time')), ], ), + 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='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)), + ], + options={ + 'verbose_name': 'Hiring Agency', + 'verbose_name_plural': 'Hiring Agencies', + 'ordering': ['name'], + }, + ), + 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'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')), + ], + options={ + 'verbose_name': 'Source', + 'verbose_name_plural': 'Sources', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ZoomMeeting', + 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')), + ('topic', models.CharField(max_length=255, verbose_name='Topic')), + ('meeting_id', models.CharField(max_length=20, unique=True, verbose_name='Meeting ID')), + ('start_time', models.DateTimeField(verbose_name='Start Time')), + ('duration', models.PositiveIntegerField(verbose_name='Duration')), + ('timezone', models.CharField(max_length=50, verbose_name='Timezone')), + ('join_url', models.URLField(verbose_name='Join URL')), + ('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')), + ('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')), + ('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')), + ('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')), + ('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')), + ], + options={ + 'abstract': False, + }, + ), + 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='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)), + ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), + ('applicant_email', models.EmailField(blank=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)), + ], + options={ + 'verbose_name': 'Form Submission', + 'verbose_name_plural': 'Form Submissions', + 'ordering': ['-submitted_at'], + }, + ), + 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', + }, + ), + 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=True, help_text='Whether this template is active')), + ('created_by', models.ForeignKey(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.AddField( + model_name='formsubmission', + name='template', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'), + ), + 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='Candidate', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('email', models.EmailField(max_length=254)), - ('resume', models.FileField(upload_to='resumes/')), - ('parsed_summary', models.TextField(blank=True)), - ('status', models.CharField(default='Applied', max_length=100)), + ('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')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('phone', models.CharField(max_length=20, verbose_name='Phone')), + ('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')), + ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), + ('applied', models.BooleanField(default=False, verbose_name='Applied')), + ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')), + ('exam_date', models.DateField(blank=True, null=True, verbose_name='Exam Date')), + ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')), + ('interview_date', models.DateField(blank=True, null=True, verbose_name='Interview Date')), + ('interview_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, 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')], max_length=100, null=True, verbose_name='Offer Status')), + ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), + ('match_score', models.IntegerField(blank=True, null=True)), + ('strengths', models.TextField(blank=True)), + ('weaknesses', models.TextField(blank=True)), + ('criteria_checklist', models.JSONField(blank=True, default=dict)), + ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), + ], + options={ + 'verbose_name': 'Candidate', + 'verbose_name_plural': 'Candidates', + }, + ), + migrations.CreateModel( + name='JobPosting', + fields=[ + ('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', models.TextField(help_text='Full job description including responsibilities and requirements')), + ('qualifications', models.TextField(blank=True, help_text='Required qualifications and skills')), + ('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)), + ('benefits', models.TextField(blank=True, help_text='Benefits offered')), + ('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), + ('application_deadline', models.DateField(blank=True, null=True)), + ('application_instructions', models.TextField(blank=True, help_text='Special instructions for applicants')), + ('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), + ('status', models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True)), + ('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)), + ('published_at', models.DateTimeField(blank=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)), + ('start_date', models.DateField(blank=True, help_text='Desired start date', null=True)), + ('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')), + ('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')), + ], + options={ + 'verbose_name': 'Job Posting', + 'verbose_name_plural': 'Job Postings', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='InterviewSchedule', + 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')), + ('start_date', models.DateField(verbose_name='Start Date')), + ('end_date', models.DateField(verbose_name='End Date')), + ('working_days', models.JSONField(verbose_name='Working Days')), + ('start_time', models.TimeField(verbose_name='Start Time')), + ('end_time', models.TimeField(verbose_name='End Time')), + ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), + ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), ('created_at', models.DateTimeField(auto_now_add=True)), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.job')), + ('breaks', models.ManyToManyField(blank=True, related_name='schedules', to='recruitment.breaktime')), + ('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')), + ], + options={ + 'abstract': False, + }, + ), + 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.AddField( + model_name='candidate', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'), + ), + 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=10, 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='TrainingMaterial', + 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=255, verbose_name='Title')), + ('content', models.TextField(blank=True, verbose_name='Content')), + ('video_link', models.URLField(blank=True, verbose_name='Video Link')), + ('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')), + ], + options={ + 'verbose_name': 'Training Material', + 'verbose_name_plural': 'Training Materials', + }, + ), + migrations.CreateModel( + name='ScheduledInterview', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('interview_date', models.DateField(verbose_name='Interview Date')), + ('interview_time', models.TimeField(verbose_name='Interview Time')), + ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')), + ('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')), + ('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')), ], ), ] diff --git a/recruitment/migrations/0002_candidate_address.py b/recruitment/migrations/0002_candidate_address.py new file mode 100644 index 0000000..c9713a1 --- /dev/null +++ b/recruitment/migrations/0002_candidate_address.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.6 on 2025-10-08 17:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='candidate', + name='address', + field=models.TextField(default='', max_length=200, verbose_name='Address'), + preserve_default=False, + ), + ] diff --git a/recruitment/migrations/0002_trainingmaterial.py b/recruitment/migrations/0002_trainingmaterial.py deleted file mode 100644 index 49aa411..0000000 --- a/recruitment/migrations/0002_trainingmaterial.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.1 on 2025-05-18 17:32 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='TrainingMaterial', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('content', models.TextField(blank=True)), - ('video_link', models.URLField(blank=True)), - ('file', models.FileField(blank=True, upload_to='training_materials/')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/recruitment/migrations/0003_candidate_updated_at_job_updated_at_and_more.py b/recruitment/migrations/0003_candidate_updated_at_job_updated_at_and_more.py deleted file mode 100644 index 81c6037..0000000 --- a/recruitment/migrations/0003_candidate_updated_at_job_updated_at_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.1 on 2025-05-18 18:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_trainingmaterial'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='updated_at', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='job', - name='updated_at', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='trainingmaterial', - name='updated_at', - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/recruitment/migrations/0003_scheduledinterview_slug.py b/recruitment/migrations/0003_scheduledinterview_slug.py new file mode 100644 index 0000000..364117e --- /dev/null +++ b/recruitment/migrations/0003_scheduledinterview_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.6 on 2025-10-08 17:47 + +import django_extensions.db.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_candidate_address'), + ] + + operations = [ + migrations.AddField( + model_name='scheduledinterview', + name='slug', + field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), + ), + ] diff --git a/recruitment/migrations/0004_remove_candidate_status_candidate_applied.py b/recruitment/migrations/0004_remove_candidate_status_candidate_applied.py deleted file mode 100644 index 123ccca..0000000 --- a/recruitment/migrations/0004_remove_candidate_status_candidate_applied.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.1 on 2025-05-18 18:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_candidate_updated_at_job_updated_at_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidate', - name='status', - ), - migrations.AddField( - model_name='candidate', - name='applied', - field=models.BooleanField(default=False), - ), - ] diff --git a/recruitment/migrations/0005_zoommeeting.py b/recruitment/migrations/0005_zoommeeting.py deleted file mode 100644 index fea9cc1..0000000 --- a/recruitment/migrations/0005_zoommeeting.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-29 09:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_remove_candidate_status_candidate_applied'), - ] - - operations = [ - migrations.CreateModel( - name='ZoomMeeting', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('topic', models.CharField(max_length=255)), - ('meeting_id', models.CharField(max_length=20, unique=True)), - ('start_time', models.DateTimeField()), - ('duration', models.PositiveIntegerField()), - ('timezone', models.CharField(max_length=50)), - ('join_url', models.URLField()), - ('password', models.CharField(blank=True, max_length=50, null=True)), - ('host_email', models.EmailField(max_length=254)), - ('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended')], default='waiting', max_length=10)), - ('host_video', models.BooleanField(default=True)), - ('participant_video', models.BooleanField(default=True)), - ('join_before_host', models.BooleanField(default=False)), - ('mute_upon_entry', models.BooleanField(default=False)), - ('waiting_room', models.BooleanField(default=False)), - ('zoom_gateway_response', models.JSONField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - ] diff --git a/recruitment/migrations/0006_jobposting_alter_candidate_options_alter_job_options_and_more.py b/recruitment/migrations/0006_jobposting_alter_candidate_options_alter_job_options_and_more.py deleted file mode 100644 index cbfd137..0000000 --- a/recruitment/migrations/0006_jobposting_alter_candidate_options_alter_job_options_and_more.py +++ /dev/null @@ -1,318 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-02 14:14 - -import django.core.validators -import django.db.models.deletion -import recruitment.validators -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_zoommeeting'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='JobPosting', - fields=[ - ('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='United States', max_length=100)), - ('description', models.TextField(help_text='Full job description including responsibilities and requirements')), - ('qualifications', models.TextField(blank=True, help_text='Required qualifications and skills')), - ('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)), - ('benefits', models.TextField(blank=True, help_text='Benefits offered')), - ('application_url', models.URLField(help_text='URL where candidates apply', validators=[django.core.validators.URLValidator()])), - ('application_deadline', models.DateField(blank=True, null=True)), - ('application_instructions', models.TextField(blank=True, help_text='Special instructions for applicants')), - ('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('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'), ('ARCHIVED', 'Archived')], 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)), - ('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)), - ('start_date', models.DateField(blank=True, help_text='Desired start date', null=True)), - ('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')), - ], - options={ - 'verbose_name': 'Job Posting', - 'verbose_name_plural': 'Job Postings', - 'ordering': ['-created_at'], - }, - ), - migrations.AlterModelOptions( - name='candidate', - options={'verbose_name': 'Candidate', 'verbose_name_plural': 'Candidates'}, - ), - migrations.AlterModelOptions( - name='job', - options={'verbose_name': 'Job', 'verbose_name_plural': 'Jobs'}, - ), - migrations.AlterModelOptions( - name='trainingmaterial', - options={'verbose_name': 'Training Material', 'verbose_name_plural': 'Training Materials'}, - ), - migrations.RemoveField( - model_name='zoommeeting', - name='host_email', - ), - migrations.RemoveField( - model_name='zoommeeting', - name='host_video', - ), - migrations.RemoveField( - model_name='zoommeeting', - name='password', - ), - migrations.RemoveField( - model_name='zoommeeting', - name='status', - ), - migrations.AddField( - model_name='candidate', - name='exam_date', - field=models.DateField(blank=True, null=True, verbose_name='Exam Date'), - ), - migrations.AddField( - model_name='candidate', - name='exam_status', - field=models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status'), - ), - migrations.AddField( - model_name='candidate', - name='first_name', - field=models.CharField(default='user', max_length=255, verbose_name='First Name'), - preserve_default=False, - ), - migrations.AddField( - model_name='candidate', - name='interview_date', - field=models.DateField(blank=True, null=True, verbose_name='Interview Date'), - ), - migrations.AddField( - model_name='candidate', - name='interview_status', - field=models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Interview Status'), - ), - migrations.AddField( - model_name='candidate', - name='join_date', - field=models.DateField(blank=True, null=True, verbose_name='Join Date'), - ), - migrations.AddField( - model_name='candidate', - name='last_name', - field=models.CharField(default='user', max_length=255, verbose_name='Last Name'), - preserve_default=False, - ), - migrations.AddField( - model_name='candidate', - name='offer_date', - field=models.DateField(blank=True, null=True, verbose_name='Offer Date'), - ), - migrations.AddField( - model_name='candidate', - name='offer_status', - field=models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status'), - ), - migrations.AddField( - model_name='candidate', - name='phone', - field=models.CharField(default='0569874562', max_length=20, verbose_name='Phone'), - preserve_default=False, - ), - migrations.AddField( - model_name='candidate', - name='stage', - field=models.CharField(default='Applied', max_length=100, verbose_name='Stage'), - ), - migrations.AlterField( - model_name='candidate', - name='applied', - field=models.BooleanField(default=False, verbose_name='Applied'), - ), - migrations.AlterField( - model_name='candidate', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='candidate', - name='email', - field=models.EmailField(max_length=254, verbose_name='Email'), - ), - migrations.AlterField( - model_name='candidate', - name='name', - field=models.CharField(max_length=255, verbose_name='Name'), - ), - migrations.AlterField( - model_name='candidate', - name='parsed_summary', - field=models.TextField(blank=True, verbose_name='Parsed Summary'), - ), - migrations.AlterField( - model_name='candidate', - name='resume', - field=models.FileField(upload_to='resumes/', verbose_name='Resume'), - ), - migrations.AlterField( - model_name='candidate', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AlterField( - model_name='job', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='job', - name='description_ar', - field=models.TextField(verbose_name='Description Arabic'), - ), - migrations.AlterField( - model_name='job', - name='description_en', - field=models.TextField(verbose_name='Description English'), - ), - migrations.AlterField( - model_name='job', - name='is_published', - field=models.BooleanField(default=False, verbose_name='Published'), - ), - migrations.AlterField( - model_name='job', - name='posted_to_linkedin', - field=models.BooleanField(default=False, verbose_name='Posted to LinkedIn'), - ), - migrations.AlterField( - model_name='job', - name='title', - field=models.CharField(max_length=255, verbose_name='Title'), - ), - migrations.AlterField( - model_name='job', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='content', - field=models.TextField(blank=True, verbose_name='Content'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='file', - field=models.FileField(blank=True, upload_to='training_materials/', verbose_name='File'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='title', - field=models.CharField(max_length=255, verbose_name='Title'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AlterField( - model_name='trainingmaterial', - name='video_link', - field=models.URLField(blank=True, verbose_name='Video Link'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='duration', - field=models.PositiveIntegerField(verbose_name='Duration'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='join_before_host', - field=models.BooleanField(default=False, verbose_name='Join Before Host'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='join_url', - field=models.URLField(verbose_name='Join URL'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='meeting_id', - field=models.CharField(max_length=20, unique=True, verbose_name='Meeting ID'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='mute_upon_entry', - field=models.BooleanField(default=False, verbose_name='Mute Upon Entry'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='participant_video', - field=models.BooleanField(default=True, verbose_name='Participant Video'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='start_time', - field=models.DateTimeField(verbose_name='Start Time'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='timezone', - field=models.CharField(max_length=50, verbose_name='Timezone'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='topic', - field=models.CharField(max_length=255, verbose_name='Topic'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='waiting_room', - field=models.BooleanField(default=False, verbose_name='Waiting Room'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='zoom_gateway_response', - field=models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response'), - ), - migrations.AlterField( - model_name='candidate', - name='job', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'), - ), - ] diff --git a/recruitment/migrations/0007_alter_jobposting_status.py b/recruitment/migrations/0007_alter_jobposting_status.py deleted file mode 100644 index 162e53b..0000000 --- a/recruitment/migrations/0007_alter_jobposting_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-02 14:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_jobposting_alter_candidate_options_alter_job_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='status', - field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True), - ), - ] diff --git a/recruitment/migrations/0008_jobposting_published_at_alter_jobposting_status.py b/recruitment/migrations/0008_jobposting_published_at_alter_jobposting_status.py deleted file mode 100644 index 5ab4865..0000000 --- a/recruitment/migrations/0008_jobposting_published_at_alter_jobposting_status.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-02 14:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_alter_jobposting_status'), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='published_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='jobposting', - name='status', - field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True), - ), - ] diff --git a/recruitment/migrations/0009_candidate_slug_job_slug_jobposting_slug_and_more.py b/recruitment/migrations/0009_candidate_slug_job_slug_jobposting_slug_and_more.py deleted file mode 100644 index e4ad2eb..0000000 --- a/recruitment/migrations/0009_candidate_slug_job_slug_jobposting_slug_and_more.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-02 14:39 - -import django_extensions.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_jobposting_published_at_alter_jobposting_status'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='job', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='jobposting', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='trainingmaterial', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='zoommeeting', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AlterField( - model_name='jobposting', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='jobposting', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - ] diff --git a/recruitment/migrations/0010_remove_candidate_name.py b/recruitment/migrations/0010_remove_candidate_name.py deleted file mode 100644 index e302ff2..0000000 --- a/recruitment/migrations/0010_remove_candidate_name.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-02 15:16 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0009_candidate_slug_job_slug_jobposting_slug_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidate', - name='name', - ), - ] diff --git a/recruitment/migrations/0011_alter_candidate_stage.py b/recruitment/migrations/0011_alter_candidate_stage.py deleted file mode 100644 index 1f8c15f..0000000 --- a/recruitment/migrations/0011_alter_candidate_stage.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-02 16:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0010_remove_candidate_name'), - ] - - operations = [ - migrations.AlterField( - model_name='candidate', - name='stage', - field=models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage'), - ), - ] diff --git a/recruitment/migrations/0012_form_formsubmission_uploadedfile.py b/recruitment/migrations/0012_form_formsubmission_uploadedfile.py deleted file mode 100644 index 6ea80ba..0000000 --- a/recruitment/migrations/0012_form_formsubmission_uploadedfile.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-04 12:39 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0011_alter_candidate_stage'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Form', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=200)), - ('description', models.TextField(blank=True)), - ('structure', models.JSONField(default=dict)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('is_active', models.BooleanField(default=True)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='FormSubmission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('submission_data', models.JSONField(default=dict)), - ('submitted_at', models.DateTimeField(auto_now_add=True)), - ('ip_address', models.GenericIPAddressField(blank=True, null=True)), - ('user_agent', models.TextField(blank=True)), - ('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.form')), - ], - options={ - 'ordering': ['-submitted_at'], - }, - ), - migrations.CreateModel( - name='UploadedFile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('field_id', models.CharField(max_length=100)), - ('file', models.FileField(upload_to='form_uploads/%Y/%m/%d/')), - ('original_filename', models.CharField(max_length=255)), - ('uploaded_at', models.DateTimeField(auto_now_add=True)), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='recruitment.formsubmission')), - ], - ), - ] diff --git a/recruitment/migrations/0013_candidate_criteria_checklist_candidate_match_score_and_more.py b/recruitment/migrations/0013_candidate_criteria_checklist_candidate_match_score_and_more.py deleted file mode 100644 index b1cfc32..0000000 --- a/recruitment/migrations/0013_candidate_criteria_checklist_candidate_match_score_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-05 13:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0012_form_formsubmission_uploadedfile'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='criteria_checklist', - field=models.JSONField(blank=True, default=dict), - ), - migrations.AddField( - model_name='candidate', - name='match_score', - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='candidate', - name='strengths', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='candidate', - name='weaknesses', - field=models.TextField(blank=True), - ), - ] diff --git a/recruitment/migrations/0013_formfield_formstage_remove_formsubmission_form_and_more.py b/recruitment/migrations/0013_formfield_formstage_remove_formsubmission_form_and_more.py deleted file mode 100644 index 9157a8b..0000000 --- a/recruitment/migrations/0013_formfield_formstage_remove_formsubmission_form_and_more.py +++ /dev/null @@ -1,156 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-05 09:50 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0012_form_formsubmission_uploadedfile'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='FormField', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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)')), - ], - options={ - 'verbose_name': 'Form Field', - 'verbose_name_plural': 'Form Fields', - 'ordering': ['order'], - }, - ), - migrations.CreateModel( - name='FormStage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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.RemoveField( - model_name='formsubmission', - name='form', - ), - migrations.RemoveField( - model_name='uploadedfile', - name='submission', - ), - migrations.AlterModelOptions( - name='formsubmission', - options={'ordering': ['-submitted_at'], 'verbose_name': 'Form Submission', 'verbose_name_plural': 'Form Submissions'}, - ), - migrations.RemoveField( - model_name='formsubmission', - name='ip_address', - ), - migrations.RemoveField( - model_name='formsubmission', - name='submission_data', - ), - migrations.RemoveField( - model_name='formsubmission', - name='user_agent', - ), - migrations.AddField( - model_name='formsubmission', - name='applicant_email', - field=models.EmailField(blank=True, help_text='Email of the applicant', max_length=254), - ), - migrations.AddField( - model_name='formsubmission', - name='applicant_name', - field=models.CharField(blank=True, help_text='Name of the applicant', max_length=200), - ), - migrations.AddField( - model_name='formsubmission', - name='submitted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL), - ), - migrations.CreateModel( - name='FieldResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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/')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), - ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), - ], - options={ - 'verbose_name': 'Field Response', - 'verbose_name_plural': 'Field Responses', - }, - ), - migrations.AddField( - model_name='formfield', - name='stage', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage'), - ), - migrations.CreateModel( - name='FormTemplate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('is_active', models.BooleanField(default=True, help_text='Whether this template is active')), - ('created_by', models.ForeignKey(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.AddField( - model_name='formstage', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'), - ), - migrations.AddField( - model_name='formsubmission', - name='template', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'), - preserve_default=False, - ), - migrations.CreateModel( - name='SharedFormTemplate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('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.DeleteModel( - name='Form', - ), - migrations.DeleteModel( - name='UploadedFile', - ), - ] diff --git a/recruitment/migrations/0014_source_jobposting_source.py b/recruitment/migrations/0014_source_jobposting_source.py deleted file mode 100644 index cfda728..0000000 --- a/recruitment/migrations/0014_source_jobposting_source.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-05 16:11 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0013_candidate_criteria_checklist_candidate_match_score_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Source', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(choices=[('ATS', 'Applicant Tracking System'), ('ERP', 'ERP system')], max_length=100, verbose_name='Source Type')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Source', - 'verbose_name_plural': 'Sources', - }, - ), - migrations.AddField( - model_name='jobposting', - name='source', - field=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'), - ), - ] diff --git a/recruitment/migrations/0015_hiringagency_candidate_submitted_by_agency_and_more.py b/recruitment/migrations/0015_hiringagency_candidate_submitted_by_agency_and_more.py deleted file mode 100644 index 91ce2f3..0000000 --- a/recruitment/migrations/0015_hiringagency_candidate_submitted_by_agency_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-05 16:46 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0014_source_jobposting_source'), - ] - - operations = [ - migrations.CreateModel( - name='HiringAgency', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'Hiring Agency', - 'verbose_name_plural': 'Hiring Agencies', - 'ordering': ['name'], - }, - ), - migrations.AddField( - model_name='candidate', - name='submitted_by_agency', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency'), - ), - migrations.AddField( - model_name='jobposting', - name='hiring_agency', - field=models.ForeignKey(blank=True, help_text='External agency responsible for sourcing candidates for this role', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'), - ), - ] diff --git a/recruitment/migrations/0016_alter_source_options_hiringagency_address_and_more.py b/recruitment/migrations/0016_alter_source_options_hiringagency_address_and_more.py deleted file mode 100644 index 7354ad0..0000000 --- a/recruitment/migrations/0016_alter_source_options_hiringagency_address_and_more.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-06 10:48 - -import django_countries.fields -import django_extensions.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0015_hiringagency_candidate_submitted_by_agency_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='source', - options={'ordering': ['name'], 'verbose_name': 'Source', 'verbose_name_plural': 'Sources'}, - ), - migrations.AddField( - model_name='hiringagency', - name='address', - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name='hiringagency', - name='country', - field=django_countries.fields.CountryField(blank=True, max_length=2, null=True), - ), - migrations.AddField( - model_name='hiringagency', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AlterField( - model_name='hiringagency', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='hiringagency', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.RemoveField( - model_name='jobposting', - name='hiring_agency', - ), - migrations.AlterField( - model_name='source', - name='name', - field=models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name'), - ), - migrations.AddField( - model_name='jobposting', - name='hiring_agency', - field=models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', null=True, related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'), - ), - ] diff --git a/recruitment/migrations/0017_alter_jobposting_hiring_agency.py b/recruitment/migrations/0017_alter_jobposting_hiring_agency.py deleted file mode 100644 index da4e051..0000000 --- a/recruitment/migrations/0017_alter_jobposting_hiring_agency.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-06 10:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0016_alter_source_options_hiringagency_address_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='hiring_agency', - field=models.ManyToManyField(help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'), - ), - ] diff --git a/recruitment/migrations/0018_alter_jobposting_hiring_agency.py b/recruitment/migrations/0018_alter_jobposting_hiring_agency.py deleted file mode 100644 index 07c349f..0000000 --- a/recruitment/migrations/0018_alter_jobposting_hiring_agency.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-06 11:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0017_alter_jobposting_hiring_agency'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='hiring_agency', - field=models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'), - ), - ] diff --git a/recruitment/migrations/0019_merge_20251006_1224.py b/recruitment/migrations/0019_merge_20251006_1224.py deleted file mode 100644 index a706fa2..0000000 --- a/recruitment/migrations/0019_merge_20251006_1224.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-06 12:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0013_formfield_formstage_remove_formsubmission_form_and_more'), - ('recruitment', '0018_alter_jobposting_hiring_agency'), - ] - - operations = [ - ] diff --git a/recruitment/migrations/0020_delete_job.py b/recruitment/migrations/0020_delete_job.py deleted file mode 100644 index 80b089f..0000000 --- a/recruitment/migrations/0020_delete_job.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-06 13:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0019_merge_20251006_1224'), - ] - - operations = [ - migrations.DeleteModel( - name='Job', - ), - ] diff --git a/recruitment/migrations/0021_source_api_key_source_api_secret_source_description_and_more.py b/recruitment/migrations/0021_source_api_key_source_api_secret_source_description_and_more.py deleted file mode 100644 index 91a68ac..0000000 --- a/recruitment/migrations/0021_source_api_key_source_api_secret_source_description_and_more.py +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-06 14:10 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0020_delete_job'), - ] - - operations = [ - migrations.AddField( - model_name='source', - name='api_key', - field=models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key'), - ), - migrations.AddField( - model_name='source', - name='api_secret', - field=models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret'), - ), - migrations.AddField( - model_name='source', - name='description', - field=models.TextField(blank=True, help_text='A description of the source', verbose_name='Description'), - ), - migrations.AddField( - model_name='source', - name='integration_version', - field=models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version'), - ), - migrations.AddField( - model_name='source', - name='ip_address', - field=models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address'), - ), - migrations.AddField( - model_name='source', - name='is_active', - field=models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active'), - ), - migrations.AddField( - model_name='source', - name='last_sync_at', - field=models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At'), - ), - migrations.AddField( - model_name='source', - name='source_type', - field=models.CharField(default='erp', help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type'), - preserve_default=False, - ), - migrations.AddField( - model_name='source', - name='sync_status', - field=models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status'), - ), - migrations.AddField( - model_name='source', - name='trusted_ips', - field=models.GenericIPAddressField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses'), - ), - migrations.CreateModel( - name='IntegrationLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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=10, 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)')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('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'], - }, - ), - ] diff --git a/recruitment/migrations/0022_alter_source_trusted_ips.py b/recruitment/migrations/0022_alter_source_trusted_ips.py deleted file mode 100644 index facbfec..0000000 --- a/recruitment/migrations/0022_alter_source_trusted_ips.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-06 14:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0021_source_api_key_source_api_secret_source_description_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='source', - name='trusted_ips', - field=models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses'), - ), - ] diff --git a/recruitment/migrations/0023_alter_jobposting_application_url_and_more.py b/recruitment/migrations/0023_alter_jobposting_application_url_and_more.py deleted file mode 100644 index a1d7ce3..0000000 --- a/recruitment/migrations/0023_alter_jobposting_application_url_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-06 14:38 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0022_alter_source_trusted_ips'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='application_url', - field=models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()]), - ), - migrations.AlterField( - model_name='jobposting', - name='location_country', - field=models.CharField(default='Saudia Arabia', max_length=100), - ), - ] diff --git a/recruitment/migrations/0024_fieldresponse_created_at_fieldresponse_slug_and_more.py b/recruitment/migrations/0024_fieldresponse_created_at_fieldresponse_slug_and_more.py deleted file mode 100644 index 439ae6d..0000000 --- a/recruitment/migrations/0024_fieldresponse_created_at_fieldresponse_slug_and_more.py +++ /dev/null @@ -1,141 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-07 10:19 - -import django.db.models.deletion -import django.utils.timezone -import django_extensions.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0023_alter_jobposting_application_url_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='fieldresponse', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), - preserve_default=False, - ), - migrations.AddField( - model_name='fieldresponse', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='fieldresponse', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AddField( - model_name='formfield', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), - preserve_default=False, - ), - migrations.AddField( - model_name='formfield', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='formfield', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AddField( - model_name='formstage', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), - preserve_default=False, - ), - migrations.AddField( - model_name='formstage', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='formstage', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AddField( - model_name='formsubmission', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), - preserve_default=False, - ), - migrations.AddField( - model_name='formsubmission', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='formsubmission', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AddField( - model_name='formtemplate', - name='job', - field=models.OneToOneField(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'), - preserve_default=False, - ), - migrations.AddField( - model_name='formtemplate', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='integrationlog', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='integrationlog', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AddField( - model_name='sharedformtemplate', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='sharedformtemplate', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AddField( - model_name='source', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='source', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AlterField( - model_name='formtemplate', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='formtemplate', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - migrations.AlterField( - model_name='integrationlog', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='sharedformtemplate', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - ] diff --git a/recruitment/migrations/0025_formfield_max_files_formfield_multiple_files.py b/recruitment/migrations/0025_formfield_max_files_formfield_multiple_files.py deleted file mode 100644 index 3b07f37..0000000 --- a/recruitment/migrations/0025_formfield_max_files_formfield_multiple_files.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-07 12:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0024_fieldresponse_created_at_fieldresponse_slug_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='formfield', - name='max_files', - field=models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)'), - ), - migrations.AddField( - model_name='formfield', - name='multiple_files', - field=models.BooleanField(default=False, help_text='Allow multiple files to be uploaded'), - ), - ] diff --git a/recruitment/migrations/0026_interviewschedule_scheduledinterview.py b/recruitment/migrations/0026_interviewschedule_scheduledinterview.py deleted file mode 100644 index 08541f3..0000000 --- a/recruitment/migrations/0026_interviewschedule_scheduledinterview.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-07 14:12 - -import django.db.models.deletion -import django_extensions.db.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0025_formfield_max_files_formfield_multiple_files'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='InterviewSchedule', - 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(verbose_name='Start Date')), - ('end_date', models.DateField(verbose_name='End Date')), - ('working_days', models.JSONField(verbose_name='Working Days')), - ('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)')), - ('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')), - ], - options={ - 'abstract': False, - }, - ), - 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(verbose_name='Interview Date')), - ('interview_time', models.TimeField(verbose_name='Interview Time')), - ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)), - ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')), - ('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')), - ('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc index 72c4c4a..492600e 100644 Binary files a/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc and b/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-313.pyc b/recruitment/migrations/__pycache__/__init__.cpython-313.pyc index bde0d3d..f41c210 100644 Binary files a/recruitment/migrations/__pycache__/__init__.cpython-313.pyc and b/recruitment/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index 187e0a2..40ede85 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -9,17 +9,21 @@ from django.core.exceptions import ValidationError from django_countries.fields import CountryField from django.urls import reverse + class Profile(models.Model): profile_image=models.ImageField(null=True,blank=True,upload_to='profile_pic/') user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile') class Base(models.Model): - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) - updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at')) - slug = RandomCharField(length=8, unique=True, editable=False, verbose_name=_('Slug')) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at")) + slug = RandomCharField( + length=8, unique=True, editable=False, verbose_name=_("Slug") + ) class Meta: abstract = True + # # Create your models here. # class Job(Base): # title = models.CharField(max_length=255, verbose_name=_('Title')) @@ -37,95 +41,133 @@ class Base(models.Model): # def __str__(self): # return self.title + class JobPosting(Base): # Basic Job Information JOB_TYPES = [ - ('FULL_TIME', 'Full-time'), - ('PART_TIME', 'Part-time'), - ('CONTRACT', 'Contract'), - ('INTERNSHIP', 'Internship'), - ('FACULTY', 'Faculty'), - ('TEMPORARY', 'Temporary'), + ("FULL_TIME", "Full-time"), + ("PART_TIME", "Part-time"), + ("CONTRACT", "Contract"), + ("INTERNSHIP", "Internship"), + ("FACULTY", "Faculty"), + ("TEMPORARY", "Temporary"), ] WORKPLACE_TYPES = [ - ('ON_SITE', 'On-site'), - ('REMOTE', 'Remote'), - ('HYBRID', 'Hybrid'), + ("ON_SITE", "On-site"), + ("REMOTE", "Remote"), + ("HYBRID", "Hybrid"), ] # Core Fields title = models.CharField(max_length=200) department = models.CharField(max_length=100, blank=True) - job_type = models.CharField(max_length=20, choices=JOB_TYPES, default='FULL_TIME') - workplace_type = models.CharField(max_length=20, choices=WORKPLACE_TYPES, default='ON_SITE') + job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="FULL_TIME") + workplace_type = models.CharField( + max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE" + ) # Location location_city = models.CharField(max_length=100, blank=True) location_state = models.CharField(max_length=100, blank=True) - location_country = models.CharField(max_length=100, default='Saudia Arabia') + location_country = models.CharField(max_length=100, default="Saudia Arabia") # Job Details - description = models.TextField(help_text="Full job description including responsibilities and requirements") - qualifications = models.TextField(blank=True, help_text="Required qualifications and skills") - salary_range = models.CharField(max_length=200, blank=True, help_text="e.g., $60,000 - $80,000") + description = models.TextField( + help_text="Full job description including responsibilities and requirements" + ) + qualifications = models.TextField( + blank=True, help_text="Required qualifications and skills" + ) + salary_range = models.CharField( + max_length=200, blank=True, help_text="e.g., $60,000 - $80,000" + ) benefits = models.TextField(blank=True, help_text="Benefits offered") - # Application Information - application_url = models.URLField(validators=[URLValidator()], help_text="URL where candidates apply",null=True, blank=True) + application_url = models.URLField( + validators=[URLValidator()], + help_text="URL where candidates apply", + null=True, + blank=True, + ) application_deadline = models.DateField(null=True, blank=True) - application_instructions = models.TextField(blank=True, help_text="Special instructions for applicants") + application_instructions = models.TextField( + blank=True, help_text="Special instructions for applicants" + ) # Internal Tracking internal_job_id = models.CharField(max_length=50, primary_key=True, editable=False) - created_by = models.CharField(max_length=100, blank=True, help_text="Name of person who created this job") + created_by = models.CharField( + max_length=100, blank=True, help_text="Name of person who created this job" + ) # Status Fields STATUS_CHOICES = [ - ('DRAFT', 'Draft'), - ('PUBLISHED', 'Published'), - ('CLOSED', 'Closed'), - ('ARCHIVED', 'Archived'), + ("DRAFT", "Draft"), + ("PUBLISHED", "Published"), + ("CLOSED", "Closed"), + ("ARCHIVED", "Archived"), ] - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='DRAFT',null=True, blank=True) + status = models.CharField( + max_length=20, choices=STATUS_CHOICES, default="DRAFT", null=True, blank=True + ) - #hashtags for social media - hash_tags = models.CharField(max_length=200, blank=True, help_text="Comma-separated hashtags for linkedin post like #hiring,#jobopening",validators=[validate_hash_tags]) + # hashtags for social media + hash_tags = models.CharField( + max_length=200, + blank=True, + help_text="Comma-separated hashtags for linkedin post like #hiring,#jobopening", + validators=[validate_hash_tags], + ) # LinkedIn Integration Fields - linkedin_post_id = models.CharField(max_length=200, blank=True, help_text="LinkedIn post ID after posting") - linkedin_post_url = models.URLField(blank=True, help_text="Direct URL to LinkedIn post") + linkedin_post_id = models.CharField( + max_length=200, blank=True, help_text="LinkedIn post ID after posting" + ) + 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(max_length=50, blank=True, help_text="Status of LinkedIn posting") + linkedin_post_status = models.CharField( + max_length=50, blank=True, help_text="Status of LinkedIn posting" + ) linkedin_posted_at = models.DateTimeField(null=True, blank=True) published_at = models.DateTimeField(null=True, blank=True) # University Specific Fields - position_number = models.CharField(max_length=50, blank=True, help_text="University position number") - reporting_to = models.CharField(max_length=100, blank=True, help_text="Who this position reports to") + position_number = models.CharField( + max_length=50, blank=True, help_text="University position number" + ) + reporting_to = models.CharField( + max_length=100, blank=True, help_text="Who this position reports to" + ) start_date = models.DateField(null=True, blank=True, help_text="Desired start date") - open_positions = models.PositiveIntegerField(default=1, help_text="Number of open positions for this job") + open_positions = models.PositiveIntegerField( + default=1, help_text="Number of open positions for this job" + ) source = models.ForeignKey( - 'Source', - on_delete=models.SET_NULL, # Recommended: If a source is deleted, job's source is set to NULL - related_name='job_postings', + "Source", + on_delete=models.SET_NULL, # Recommended: If a source is deleted, job's source is set to NULL + related_name="job_postings", null=True, blank=True, - help_text="The system or channel from which this job posting originated or was first published." + help_text="The system or channel from which this job posting originated or was first published.", ) hiring_agency = models.ManyToManyField( - 'HiringAgency', + "HiringAgency", blank=True, - related_name='jobs', - verbose_name=_('Hiring Agency'), - help_text=_("External agency responsible for sourcing candidates for this role") + related_name="jobs", + verbose_name=_("Hiring Agency"), + help_text=_( + "External agency responsible for sourcing candidates for this role" + ), ) class Meta: - ordering = ['-created_at'] + ordering = ["-created_at"] verbose_name = "Job Posting" verbose_name_plural = "Job Postings" @@ -133,19 +175,24 @@ class JobPosting(Base): return f"{self.title} - {self.internal_job_id}-{self.get_status_display()}" def get_source(self): - return self.source.name if self.source else 'System' + return self.source.name if self.source else "System" + def save(self, *args, **kwargs): # Generate unique internal job ID if not exists if not self.internal_job_id: prefix = "KAAUH" year = timezone.now().year # Get next sequential number - last_job = JobPosting.objects.filter( - internal_job_id__startswith=f"{prefix}-{year}-" - ).order_by('internal_job_id').last() + last_job = ( + JobPosting.objects.filter( + internal_job_id__startswith=f"{prefix}-{year}-" + ) + .order_by("internal_job_id") + .last() + ) if last_job: - last_num = int(last_job.internal_job_id.split('-')[-1]) + last_num = int(last_job.internal_job_id.split("-")[-1]) next_num = last_num + 1 else: next_num = 1 @@ -161,9 +208,9 @@ class JobPosting(Base): parts.append(self.location_city) if self.location_state: parts.append(self.location_state) - if self.location_country and self.location_country != 'United States': + if self.location_country and self.location_country != "United States": parts.append(self.location_country) - return ', '.join(parts) if parts else 'Not specified' + return ", ".join(parts) if parts else "Not specified" def is_expired(self): """Check if application deadline has passed""" @@ -172,50 +219,85 @@ class JobPosting(Base): return False def publish(self): - self.status = 'PUBLISHED' + self.status = "PUBLISHED" self.published_at = timezone.now() - self.application_url = reverse('form_wizard', kwargs={'slug': self.form_template.slug}) + self.application_url = reverse( + "form_wizard", kwargs={"slug": self.form_template.slug} + ) self.save() class Candidate(Base): class Stage(models.TextChoices): - APPLIED = 'Applied', _('Applied') - EXAM = 'Exam', _('Exam') - INTERVIEW = 'Interview', _('Interview') - OFFER = 'Offer', _('Offer') + APPLIED = "Applied", _("Applied") + EXAM = "Exam", _("Exam") + INTERVIEW = "Interview", _("Interview") + OFFER = "Offer", _("Offer") + class ExamStatus(models.TextChoices): - PASSED = 'Passed', _('Passed') - FAILED = 'Failed', _('Failed') + PASSED = "Passed", _("Passed") + FAILED = "Failed", _("Failed") + class Status(models.TextChoices): - ACCEPTED = 'Accepted', _('Accepted') - REJECTED = 'Rejected', _('Rejected') + ACCEPTED = "Accepted", _("Accepted") + REJECTED = "Rejected", _("Rejected") # Stage transition validation constants STAGE_SEQUENCE = { - 'Applied': ['Exam', 'Interview', 'Offer'], - 'Exam': ['Interview', 'Offer'], - 'Interview': ['Offer'], - 'Offer': [] # Final stage - no further transitions + "Applied": ["Exam", "Interview", "Offer"], + "Exam": ["Interview", "Offer"], + "Interview": ["Offer"], + "Offer": [], # Final stage - no further transitions } - job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name='candidates', verbose_name=_('Job')) - first_name = models.CharField(max_length=255, verbose_name=_('First Name')) - last_name = models.CharField(max_length=255, verbose_name=_('Last Name')) - email = models.EmailField(verbose_name=_('Email')) - phone = models.CharField(max_length=20, verbose_name=_('Phone')) - resume = models.FileField(upload_to='resumes/', verbose_name=_('Resume')) - parsed_summary = models.TextField(blank=True, verbose_name=_('Parsed Summary')) - applied = models.BooleanField(default=False, verbose_name=_('Applied')) - stage = models.CharField(max_length=100, default='Applied', choices=Stage.choices, verbose_name=_('Stage')) + job = models.ForeignKey( + JobPosting, + on_delete=models.CASCADE, + related_name="candidates", + verbose_name=_("Job"), + ) + first_name = models.CharField(max_length=255, verbose_name=_("First Name")) + last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) + email = models.EmailField(verbose_name=_("Email")) + phone = models.CharField(max_length=20, verbose_name=_("Phone")) + address = models.TextField(max_length=200,verbose_name=_("Address")) + resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume")) + parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary")) + applied = models.BooleanField(default=False, verbose_name=_("Applied")) + stage = models.CharField( + max_length=100, + default="Applied", + choices=Stage.choices, + verbose_name=_("Stage"), + ) - exam_date = models.DateField(null=True, blank=True, verbose_name=_('Exam Date')) - exam_status = models.CharField(choices=ExamStatus.choices,max_length=100, null=True, blank=True, verbose_name=_('Exam Status')) - interview_date = models.DateField(null=True, blank=True, verbose_name=_('Interview Date')) - interview_status = models.CharField(choices=Status.choices,max_length=100, null=True, blank=True, verbose_name=_('Interview Status')) - offer_date = models.DateField(null=True, blank=True, verbose_name=_('Offer Date')) - offer_status = models.CharField(choices=Status.choices,max_length=100, null=True, blank=True, verbose_name=_('Offer Status')) - join_date = models.DateField(null=True, blank=True, verbose_name=_('Join Date')) + exam_date = models.DateField(null=True, blank=True, verbose_name=_("Exam Date")) + exam_status = models.CharField( + choices=ExamStatus.choices, + max_length=100, + null=True, + blank=True, + verbose_name=_("Exam Status"), + ) + interview_date = models.DateField( + null=True, blank=True, verbose_name=_("Interview Date") + ) + interview_status = models.CharField( + choices=Status.choices, + max_length=100, + null=True, + blank=True, + verbose_name=_("Interview Status"), + ) + offer_date = models.DateField(null=True, blank=True, verbose_name=_("Offer Date")) + offer_status = models.CharField( + choices=Status.choices, + max_length=100, + null=True, + blank=True, + verbose_name=_("Offer Status"), + ) + join_date = models.DateField(null=True, blank=True, verbose_name=_("Join Date")) # Scoring fields (populated by signal) match_score = models.IntegerField(null=True, blank=True) @@ -223,27 +305,32 @@ class Candidate(Base): weaknesses = models.TextField(blank=True) criteria_checklist = models.JSONField(default=dict, blank=True) - submitted_by_agency = models.ForeignKey( - 'HiringAgency', + "HiringAgency", on_delete=models.SET_NULL, null=True, blank=True, - related_name='submitted_candidates', - verbose_name=_('Submitted by Agency') + related_name="submitted_candidates", + verbose_name=_("Submitted by Agency"), ) class Meta: - verbose_name = _('Candidate') - verbose_name_plural = _('Candidates') + verbose_name = _("Candidate") + verbose_name_plural = _("Candidates") @property def name(self): return f"{self.first_name} {self.last_name}" + @property def full_name(self): return self.name + @property + def get_file_size(self): + if self.resume: + return self.resume.size + return 0 def clean(self): """Validate stage transitions""" # Only validate if this is an existing record (not being created) @@ -252,16 +339,20 @@ class Candidate(Base): allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) if self.stage not in allowed_next_stages: - raise ValidationError({ - 'stage': f'Cannot transition from "{old_stage}" to "{self.stage}". ' - f'Allowed transitions: {", ".join(allowed_next_stages) or "None (final stage)"}' - }) + raise ValidationError( + { + "stage": f'Cannot transition from "{old_stage}" to "{self.stage}". ' + f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}" + } + ) # Validate that the stage is a valid choice if self.stage not in [choice[0] for choice in self.Stage.choices]: - raise ValidationError({ - 'stage': f'Invalid stage. Must be one of: {", ".join(choice[0] for choice in self.Stage.choices)}' - }) + raise ValidationError( + { + "stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}" + } + ) def save(self, *args, **kwargs): """Override save to ensure validation is called""" @@ -271,7 +362,7 @@ class Candidate(Base): def can_transition_to(self, new_stage): """Check if a stage transition is allowed""" if not self.pk: # New record - can be in Applied stage - return new_stage == 'Applied' + return new_stage == "Applied" old_stage = self.__class__.objects.get(pk=self.pk).stage allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) @@ -280,7 +371,7 @@ class Candidate(Base): def get_available_stages(self): """Get list of stages this candidate can transition to""" if not self.pk: # New record - return ['Applied'] + return ["Applied"] old_stage = self.__class__.objects.get(pk=self.pk).stage return self.STAGE_SEQUENCE.get(old_stage, []) @@ -290,15 +381,19 @@ class Candidate(Base): class TrainingMaterial(Base): - title = models.CharField(max_length=255, verbose_name=_('Title')) - content = models.TextField(blank=True, verbose_name=_('Content')) - video_link = models.URLField(blank=True, verbose_name=_('Video Link')) - file = models.FileField(upload_to='training_materials/', blank=True, verbose_name=_('File')) - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name=_('Created by')) + title = models.CharField(max_length=255, verbose_name=_("Title")) + content = models.TextField(blank=True, verbose_name=_("Content")) + video_link = models.URLField(blank=True, verbose_name=_("Video Link")) + file = models.FileField( + upload_to="training_materials/", blank=True, verbose_name=_("File") + ) + created_by = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, verbose_name=_("Created by") + ) class Meta: - verbose_name = _('Training Material') - verbose_name_plural = _('Training Materials') + verbose_name = _("Training Material") + verbose_name_plural = _("Training Materials") def __str__(self): return self.title @@ -306,18 +401,32 @@ class TrainingMaterial(Base): class ZoomMeeting(Base): # Basic meeting details - topic = models.CharField(max_length=255, verbose_name=_('Topic')) - meeting_id = models.CharField(max_length=20, unique=True, verbose_name=_('Meeting ID')) # Unique identifier for the meeting - start_time = models.DateTimeField(verbose_name=_('Start Time')) - duration = models.PositiveIntegerField(verbose_name=_('Duration')) # Duration in minutes - timezone = models.CharField(max_length=50, verbose_name=_('Timezone')) - join_url = models.URLField(verbose_name=_('Join URL')) # URL for participants to join - participant_video = models.BooleanField(default=True, verbose_name=_('Participant Video')) - join_before_host = models.BooleanField(default=False, verbose_name=_('Join Before Host')) - mute_upon_entry = models.BooleanField(default=False, verbose_name=_('Mute Upon Entry')) - waiting_room = models.BooleanField(default=False, verbose_name=_('Waiting Room')) + topic = models.CharField(max_length=255, verbose_name=_("Topic")) + meeting_id = models.CharField( + max_length=20, unique=True, verbose_name=_("Meeting ID") + ) # Unique identifier for the meeting + start_time = models.DateTimeField(verbose_name=_("Start Time")) + duration = models.PositiveIntegerField( + verbose_name=_("Duration") + ) # Duration in minutes + timezone = models.CharField(max_length=50, verbose_name=_("Timezone")) + join_url = models.URLField( + verbose_name=_("Join URL") + ) # URL for participants to join + participant_video = models.BooleanField( + default=True, verbose_name=_("Participant Video") + ) + join_before_host = models.BooleanField( + default=False, verbose_name=_("Join Before Host") + ) + mute_upon_entry = models.BooleanField( + default=False, verbose_name=_("Mute Upon Entry") + ) + waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room")) - zoom_gateway_response = models.JSONField(blank=True, null=True, verbose_name=_('Zoom Gateway Response')) + zoom_gateway_response = models.JSONField( + blank=True, null=True, verbose_name=_("Zoom Gateway Response") + ) # Timestamps def __str__(self): @@ -328,16 +437,25 @@ class FormTemplate(Base): """ Represents a complete form template with multiple stages """ - job = models.OneToOneField(JobPosting, on_delete=models.CASCADE, related_name='form_template') + + job = models.OneToOneField( + JobPosting, on_delete=models.CASCADE, related_name="form_template" + ) name = models.CharField(max_length=200, help_text="Name of the form template") - description = models.TextField(blank=True, help_text="Description of the form template") - created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='form_templates') - is_active = models.BooleanField(default=True, help_text="Whether this template is active") + description = models.TextField( + blank=True, help_text="Description of the form template" + ) + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="form_templates" + ) + is_active = models.BooleanField( + default=True, help_text="Whether this template is active" + ) class Meta: - ordering = ['-created_at'] - verbose_name = 'Form Template' - verbose_name_plural = 'Form Templates' + ordering = ["-created_at"] + verbose_name = "Form Template" + verbose_name_plural = "Form Templates" def __str__(self): return self.name @@ -349,20 +467,26 @@ class FormTemplate(Base): return sum(stage.fields.count() for stage in self.stages.all()) - class FormStage(Base): """ Represents a stage/section within a form template """ - template = models.ForeignKey(FormTemplate, on_delete=models.CASCADE, related_name='stages') + + template = models.ForeignKey( + FormTemplate, on_delete=models.CASCADE, related_name="stages" + ) name = models.CharField(max_length=200, help_text="Name of the stage") - 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") + 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" + ) class Meta: - ordering = ['order'] - verbose_name = 'Form Stage' - verbose_name_plural = 'Form Stages' + ordering = ["order"] + verbose_name = "Form Stage" + verbose_name_plural = "Form Stages" def __str__(self): return f"{self.template.name} - {self.name}" @@ -376,60 +500,71 @@ class FormField(Base): """ Represents a single field within a form stage """ + FIELD_TYPES = [ - ('text', 'Text Input'), - ('email', 'Email'), - ('phone', 'Phone'), - ('textarea', 'Text Area'), - ('file', 'File Upload'), - ('date', 'Date Picker'), - ('select', 'Dropdown'), - ('radio', 'Radio Buttons'), - ('checkbox', 'Checkboxes'), + ("text", "Text Input"), + ("email", "Email"), + ("phone", "Phone"), + ("textarea", "Text Area"), + ("file", "File Upload"), + ("date", "Date Picker"), + ("select", "Dropdown"), + ("radio", "Radio Buttons"), + ("checkbox", "Checkboxes"), ] - stage = models.ForeignKey(FormStage, on_delete=models.CASCADE, related_name='fields') + stage = models.ForeignKey( + FormStage, on_delete=models.CASCADE, related_name="fields" + ) label = models.CharField(max_length=200, help_text="Label for the field") - field_type = models.CharField(max_length=20, choices=FIELD_TYPES, help_text="Type of the field") - placeholder = models.CharField(max_length=200, blank=True, help_text="Placeholder text") - 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") + field_type = models.CharField( + max_length=20, choices=FIELD_TYPES, help_text="Type of the field" + ) + placeholder = models.CharField( + max_length=200, blank=True, help_text="Placeholder text" + ) + 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" + ) # For selection fields (select, radio, checkbox) options = models.JSONField( default=list, blank=True, - help_text="Options for selection fields (stored as JSON array)" + help_text="Options for selection fields (stored as JSON array)", ) # For file upload fields file_types = models.CharField( max_length=200, blank=True, - help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')" + help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", ) max_file_size = models.PositiveIntegerField( - default=5, - help_text="Maximum file size in MB (default: 5MB)" + 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" + 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)" + help_text="Maximum number of files allowed (when multiple_files is True)", ) class Meta: - ordering = ['order'] - verbose_name = 'Form Field' - verbose_name_plural = 'Form Fields' + ordering = ["order"] + verbose_name = "Form Field" + verbose_name_plural = "Form Fields" def clean(self): # Validate options for selection fields - if self.field_type in ['select', 'radio', 'checkbox']: + if self.field_type in ["select", "radio", "checkbox"]: if not isinstance(self.options, list): raise ValidationError("Options must be a list for selection fields") else: @@ -438,18 +573,20 @@ class FormField(Base): self.options = [] # Validate file settings for file fields - if self.field_type == 'file': + if self.field_type == "file": if not self.file_types: - self.file_types = '.pdf,.doc,.docx' + self.file_types = ".pdf,.doc,.docx" if self.max_file_size <= 0: raise ValidationError("Max file size must be greater than 0") if self.multiple_files and self.max_files <= 0: - raise ValidationError("Max files must be greater than 0 when multiple files are allowed") + raise ValidationError( + "Max files must be greater than 0 when multiple files are allowed" + ) if not self.multiple_files: self.max_files = 1 else: # Clear file settings for non-file fields - self.file_types = '' + self.file_types = "" self.max_file_size = 0 self.multiple_files = False self.max_files = 1 @@ -463,16 +600,27 @@ class FormSubmission(Base): """ Represents a completed form submission by an applicant """ - template = models.ForeignKey(FormTemplate, on_delete=models.CASCADE, related_name='submissions') - submitted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='form_submissions') + + template = models.ForeignKey( + FormTemplate, on_delete=models.CASCADE, related_name="submissions" + ) + submitted_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="form_submissions", + ) submitted_at = models.DateTimeField(auto_now_add=True) - applicant_name = models.CharField(max_length=200, blank=True, help_text="Name of the applicant") + applicant_name = models.CharField( + max_length=200, blank=True, help_text="Name of the applicant" + ) applicant_email = models.EmailField(blank=True, help_text="Email of the applicant") class Meta: - ordering = ['-submitted_at'] - verbose_name = 'Form Submission' - verbose_name_plural = 'Form Submissions' + ordering = ["-submitted_at"] + verbose_name = "Form Submission" + verbose_name_plural = "Form Submissions" def __str__(self): return f"Submission for {self.template.name} - {self.submitted_at.strftime('%Y-%m-%d %H:%M')}" @@ -482,26 +630,48 @@ class FieldResponse(Base): """ Represents a response to a specific field in a form submission """ - submission = models.ForeignKey(FormSubmission, on_delete=models.CASCADE, related_name='responses') - field = models.ForeignKey(FormField, on_delete=models.CASCADE, related_name='responses') + + submission = models.ForeignKey( + FormSubmission, on_delete=models.CASCADE, related_name="responses" + ) + field = models.ForeignKey( + FormField, on_delete=models.CASCADE, related_name="responses" + ) # Store the response value as JSON to handle different data types - value = models.JSONField(null=True, blank=True, help_text="Response value (stored as JSON)") + value = models.JSONField( + null=True, blank=True, help_text="Response value (stored as JSON)" + ) # For file uploads, store the file path - uploaded_file = models.FileField(upload_to='form_uploads/', null=True, blank=True) + uploaded_file = models.FileField(upload_to="form_uploads/", null=True, blank=True) class Meta: - verbose_name = 'Field Response' - verbose_name_plural = 'Field Responses' + verbose_name = "Field Response" + verbose_name_plural = "Field Responses" def __str__(self): return f"Response to {self.field.label} in {self.submission}" @property + def is_file(self): + if self.uploaded_file: + return True + return False + @property + def get_file(self): + if self.is_file: + return self.uploaded_file + return None + @property + def get_file_size(self): + if self.is_file: + return self.uploaded_file.size + return 0 + @property def display_value(self): """Return a human-readable representation of the response value""" - if self.uploaded_file: + if self.is_file: return f"File: {self.uploaded_file.name}" elif self.value is None: return "" @@ -516,13 +686,18 @@ class SharedFormTemplate(Base): """ Represents a form template that can be shared across different organizations/users """ + template = models.OneToOneField(FormTemplate, on_delete=models.CASCADE) - is_public = models.BooleanField(default=False, help_text="Whether this template is publicly available") - shared_with = models.ManyToManyField(User, blank=True, related_name='shared_templates') + is_public = models.BooleanField( + default=False, help_text="Whether this template is publicly available" + ) + shared_with = models.ManyToManyField( + User, blank=True, related_name="shared_templates" + ) class Meta: - verbose_name = 'Shared Form Template' - verbose_name_plural = 'Shared Form Templates' + verbose_name = "Shared Form Template" + verbose_name_plural = "Shared Form Templates" def __str__(self): return f"Shared: {self.template.name}" @@ -532,24 +707,22 @@ class Source(Base): name = models.CharField( max_length=100, unique=True, - verbose_name=_('Source Name'), - help_text=_("e.g., ATS, ERP ") + verbose_name=_("Source Name"), + help_text=_("e.g., ATS, ERP "), ) source_type = models.CharField( - max_length=100, - verbose_name=_('Source Type'), - help_text=_("e.g., ATS, ERP ") + max_length=100, verbose_name=_("Source Type"), help_text=_("e.g., ATS, ERP ") ) description = models.TextField( blank=True, - verbose_name=_('Description'), - help_text=_("A description of the source") + verbose_name=_("Description"), + help_text=_("A description of the source"), ) ip_address = models.GenericIPAddressField( blank=True, null=True, - verbose_name=_('IP Address'), - help_text=_("The IP address of the source") + verbose_name=_("IP Address"), + help_text=_("The IP address of the source"), ) created_at = models.DateTimeField(auto_now_add=True) @@ -558,135 +731,110 @@ class Source(Base): max_length=255, blank=True, null=True, - verbose_name=_('API Key'), - help_text=_("API key for authentication (will be encrypted)") + verbose_name=_("API Key"), + help_text=_("API key for authentication (will be encrypted)"), ) api_secret = models.CharField( max_length=255, blank=True, null=True, - verbose_name=_('API Secret'), - help_text=_("API secret for authentication (will be encrypted)") + verbose_name=_("API Secret"), + help_text=_("API secret for authentication (will be encrypted)"), ) trusted_ips = models.TextField( blank=True, null=True, - verbose_name=_('Trusted IP Addresses'), - help_text=_("Comma-separated list of trusted IP addresses") + verbose_name=_("Trusted IP Addresses"), + help_text=_("Comma-separated list of trusted IP addresses"), ) is_active = models.BooleanField( default=True, - verbose_name=_('Active'), - help_text=_("Whether this source is active for integration") + verbose_name=_("Active"), + help_text=_("Whether this source is active for integration"), ) integration_version = models.CharField( max_length=50, blank=True, - verbose_name=_('Integration Version'), - help_text=_("Version of the integration protocol") + verbose_name=_("Integration Version"), + help_text=_("Version of the integration protocol"), ) last_sync_at = models.DateTimeField( null=True, blank=True, - verbose_name=_('Last Sync At'), - help_text=_("Timestamp of the last successful synchronization") + verbose_name=_("Last Sync At"), + help_text=_("Timestamp of the last successful synchronization"), ) sync_status = models.CharField( max_length=20, blank=True, choices=[ - ('IDLE', 'Idle'), - ('SYNCING', 'Syncing'), - ('ERROR', 'Error'), - ('DISABLED', 'Disabled') + ("IDLE", "Idle"), + ("SYNCING", "Syncing"), + ("ERROR", "Error"), + ("DISABLED", "Disabled"), ], - default='IDLE', - verbose_name=_('Sync Status') + default="IDLE", + verbose_name=_("Sync Status"), ) def __str__(self): return self.name - - class Meta: - verbose_name = _('Source') - verbose_name_plural = _('Sources') - ordering = ['name'] + verbose_name = _("Source") + verbose_name_plural = _("Sources") + ordering = ["name"] + class IntegrationLog(Base): """ Log all integration requests and responses for audit and debugging purposes """ + class ActionChoices(models.TextChoices): - REQUEST = 'REQUEST', _('Request') - RESPONSE = 'RESPONSE', _('Response') - ERROR = 'ERROR', _('Error') - SYNC = 'SYNC', _('Sync') - CREATE_JOB = 'CREATE_JOB', _('Create Job') - UPDATE_JOB = 'UPDATE_JOB', _('Update Job') + REQUEST = "REQUEST", _("Request") + RESPONSE = "RESPONSE", _("Response") + ERROR = "ERROR", _("Error") + SYNC = "SYNC", _("Sync") + CREATE_JOB = "CREATE_JOB", _("Create Job") + UPDATE_JOB = "UPDATE_JOB", _("Update Job") source = models.ForeignKey( Source, on_delete=models.CASCADE, - related_name='integration_logs', - verbose_name=_('Source') + related_name="integration_logs", + verbose_name=_("Source"), ) action = models.CharField( - max_length=20, - choices=ActionChoices.choices, - verbose_name=_('Action') - ) - endpoint = models.CharField( - max_length=255, - blank=True, - verbose_name=_('Endpoint') - ) - method = models.CharField( - max_length=10, - blank=True, - verbose_name=_('HTTP Method') + max_length=20, choices=ActionChoices.choices, verbose_name=_("Action") ) + endpoint = models.CharField(max_length=255, blank=True, verbose_name=_("Endpoint")) + method = models.CharField(max_length=10, blank=True, verbose_name=_("HTTP Method")) request_data = models.JSONField( - blank=True, - null=True, - verbose_name=_('Request Data') + blank=True, null=True, verbose_name=_("Request Data") ) response_data = models.JSONField( - blank=True, - null=True, - verbose_name=_('Response Data') + blank=True, null=True, verbose_name=_("Response Data") ) status_code = models.CharField( - max_length=10, - blank=True, - verbose_name=_('Status Code') - ) - error_message = models.TextField( - blank=True, - verbose_name=_('Error Message') - ) - ip_address = models.GenericIPAddressField( - verbose_name=_('IP Address') + max_length=10, blank=True, 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( - max_length=255, - blank=True, - verbose_name=_('User Agent') + max_length=255, blank=True, verbose_name=_("User Agent") ) processing_time = models.FloatField( - null=True, - blank=True, - verbose_name=_('Processing Time (seconds)') + null=True, blank=True, verbose_name=_("Processing Time (seconds)") ) def __str__(self): return f"{self.source.name} - {self.action} - {self.created_at}" class Meta: - ordering = ['-created_at'] - verbose_name = _('Integration Log') - verbose_name_plural = _('Integration Logs') + ordering = ["-created_at"] + verbose_name = _("Integration Log") + verbose_name_plural = _("Integration Logs") @property def is_successful(self): @@ -695,68 +843,105 @@ class IntegrationLog(Base): return False if self.action == self.ActionChoices.REQUEST: return True # Requests are always logged, success depends on response - if self.status_code and self.status_code.startswith('2'): + if self.status_code and self.status_code.startswith("2"): return True return False class HiringAgency(Base): - name = models.CharField(max_length=200, unique=True, verbose_name=_('Agency Name')) - contact_person = models.CharField(max_length=150, blank=True, verbose_name=_('Contact Person')) + name = models.CharField(max_length=200, unique=True, verbose_name=_("Agency Name")) + contact_person = models.CharField( + max_length=150, blank=True, verbose_name=_("Contact Person") + ) email = models.EmailField(blank=True) phone = models.CharField(max_length=20, blank=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')) - address=models.TextField(blank=True,null=True) + country = CountryField(blank=True, null=True, blank_label=_("Select country")) + address = models.TextField(blank=True, null=True) def __str__(self): return self.name class Meta: - verbose_name = _('Hiring Agency') - verbose_name_plural = _('Hiring Agencies') - ordering = ['name'] + verbose_name = _("Hiring Agency") + verbose_name_plural = _("Hiring Agencies") + ordering = ["name"] +class BreakTime(models.Model): + """Model to store break times for a schedule""" + + start_time = models.TimeField(verbose_name=_("Start Time")) + end_time = models.TimeField(verbose_name=_("End Time")) + + def __str__(self): + return f"{self.start_time} - {self.end_time}" + class InterviewSchedule(Base): """Stores the scheduling criteria for interviews""" - job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name='interview_schedules') - candidates = models.ManyToManyField(Candidate, related_name='interview_schedules') - start_date = models.DateField(verbose_name=_('Start Date')) - end_date = models.DateField(verbose_name=_('End Date')) - working_days = models.JSONField(verbose_name=_('Working Days')) # Store days of week as [0,1,2,3,4] for Mon-Fri - start_time = models.TimeField(verbose_name=_('Start Time')) - end_time = models.TimeField(verbose_name=_('End Time')) - break_start_time = models.TimeField(verbose_name=_('Break Start Time'), null=True, blank=True) - break_end_time = models.TimeField(verbose_name=_('Break End Time'), null=True, blank=True) - interview_duration = models.PositiveIntegerField(verbose_name=_('Interview Duration (minutes)')) - buffer_time = models.PositiveIntegerField(verbose_name=_('Buffer Time (minutes)'), default=0) + + job = models.ForeignKey( + JobPosting, on_delete=models.CASCADE, related_name="interview_schedules" + ) + candidates = models.ManyToManyField( + Candidate, related_name="interview_schedules" + ) + start_date = models.DateField(verbose_name=_("Start Date")) + end_date = models.DateField(verbose_name=_("End Date")) + working_days = models.JSONField( + verbose_name=_("Working Days") + ) # Store days of week as [0,1,2,3,4] for Mon-Fri + start_time = models.TimeField(verbose_name=_("Start Time")) + end_time = models.TimeField(verbose_name=_("End Time")) + breaks = models.ManyToManyField( + BreakTime, blank=True, related_name="schedules" + ) + interview_duration = models.PositiveIntegerField( + verbose_name=_("Interview Duration (minutes)") + ) + buffer_time = models.PositiveIntegerField( + verbose_name=_("Buffer Time (minutes)"), default=0 + ) created_by = models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Interview Schedule for {self.job.title}" + class ScheduledInterview(Base): """Stores individual scheduled interviews""" - candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name='scheduled_interviews') - job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name='scheduled_interviews') - zoom_meeting = models.OneToOneField(ZoomMeeting, on_delete=models.CASCADE, related_name='interview') - schedule = models.ForeignKey(InterviewSchedule, on_delete=models.CASCADE, related_name='interviews') - interview_date = models.DateField(verbose_name=_('Interview Date')) - interview_time = models.TimeField(verbose_name=_('Interview Time')) + + candidate = models.ForeignKey( + Candidate, + on_delete=models.CASCADE, + related_name="scheduled_interviews", + ) + job = models.ForeignKey( + "JobPosting", on_delete=models.CASCADE, related_name="scheduled_interviews" + ) + zoom_meeting = models.OneToOneField( + ZoomMeeting, on_delete=models.CASCADE, related_name="interview" + ) + schedule = models.ForeignKey( + InterviewSchedule, on_delete=models.CASCADE, related_name="interviews" + ) + interview_date = models.DateField(verbose_name=_("Interview Date")) + interview_time = models.TimeField(verbose_name=_("Interview Time")) status = models.CharField( max_length=20, choices=[ - ('scheduled', _('Scheduled')), - ('confirmed', _('Confirmed')), - ('cancelled', _('Cancelled')), - ('completed', _('Completed')), + ("scheduled", _("Scheduled")), + ("confirmed", _("Confirmed")), + ("cancelled", _("Cancelled")), + ("completed", _("Completed")), ], - default='scheduled' + default="scheduled", ) - + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return f"Interview with {self.candidate.name} for {self.job.title}" \ No newline at end of file + return f"Interview with {self.candidate.name} for {self.job.title}" diff --git a/recruitment/signals.py b/recruitment/signals.py index 912989d..89ec37c 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -24,15 +24,6 @@ import asyncio @receiver(post_save, sender=models.Candidate) def score_candidate_resume(sender, instance, created, **kwargs): - # Skip if no resume or OpenRouter not configured - if instance.resume: - return - if kwargs.get('update_fields') is not None: - return - - # Optional: Only re-score if resume changed (advanced: track file hash) - # For simplicity, we score on every save with a resume - try: # Get absolute file path file_path = instance.resume.path @@ -144,7 +135,7 @@ def create_default_stages(sender, instance, created, **kwargs): """ Create default resume stages when a new FormTemplate is created """ - if created: # Only run for new templates, not updates + if created: with transaction.atomic(): # Stage 1: Contact Information contact_stage = FormStage.objects.create( @@ -155,18 +146,26 @@ def create_default_stages(sender, instance, created, **kwargs): ) FormField.objects.create( stage=contact_stage, - label='Full Name', + label='First Name', field_type='text', required=True, order=0, is_predefined=True ) + FormField.objects.create( + stage=contact_stage, + label='Last Name', + field_type='text', + required=True, + order=1, + is_predefined=True + ) FormField.objects.create( stage=contact_stage, label='Email Address', field_type='email', required=True, - order=1, + order=2, is_predefined=True ) FormField.objects.create( @@ -174,7 +173,7 @@ def create_default_stages(sender, instance, created, **kwargs): label='Phone Number', field_type='phone', required=True, - order=2, + order=3, is_predefined=True ) FormField.objects.create( @@ -182,7 +181,7 @@ def create_default_stages(sender, instance, created, **kwargs): label='Address', field_type='text', required=False, - order=3, + order=4, is_predefined=True ) FormField.objects.create( @@ -190,10 +189,10 @@ def create_default_stages(sender, instance, created, **kwargs): label='Resume Upload', field_type='file', required=True, - order=4, + order=5, is_predefined=True, file_types='.pdf,.doc,.docx', - max_file_size=5 + max_file_size=1 ) # Stage 2: Resume Objective diff --git a/recruitment/urls.py b/recruitment/urls.py index 4f24bbb..4b1613b 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -63,7 +63,8 @@ urlpatterns = [ path('forms/form//', views.form_wizard_view, name='form_wizard'), path('forms/form//submit/', views.submit_form, name='submit_form'), - path('forms//submissions//', views.form_submission_details, name='form_submission_details'), + path('forms//submissions//', views.form_submission_details, name='form_submission_details'), + path('forms/template//submissions/', views.form_template_submissions_list, name='form_template_submissions_list'), path('api/templates/', views.list_form_templates, name='list_form_templates'), path('api/templates/save/', views.save_form_template, name='save_form_template'), diff --git a/recruitment/utils.py b/recruitment/utils.py index b252c57..fdfa198 100644 --- a/recruitment/utils.py +++ b/recruitment/utils.py @@ -465,7 +465,7 @@ def send_interview_email(scheduled_interview): fail_silently=False, ) -def get_available_time_slots(schedule): +def get_available_time_slots(schedule, breaks=None): """ Generate a list of available time slots based on the schedule criteria. Returns a list of dictionaries with 'date' and 'time' keys. @@ -481,8 +481,6 @@ def get_available_time_slots(schedule): # Parse times start_time = schedule.start_time end_time = schedule.end_time - break_start = schedule.break_start_time - break_end = schedule.break_end_time # Calculate slot duration (interview duration + buffer time) slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time) @@ -492,6 +490,7 @@ def get_available_time_slots(schedule): print(f"Date range: {current_date} to {end_date}") print(f"Time range: {start_time} to {end_time}") print(f"Slot duration: {slot_duration}") + print(f"Breaks: {breaks}") while current_date <= end_date: # Check if current day is a working day @@ -510,13 +509,15 @@ def get_available_time_slots(schedule): if slot_end_time > end_time: break - # Check if slot conflicts with break time + # Check if slot conflicts with any break time conflict_with_break = False - if break_start and break_end: - # Check if the slot overlaps with break time - if not (current_time >= break_end or slot_end_time <= break_start): - conflict_with_break = True - print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_start}-{break_end}") + if breaks: + for break_time in breaks: + # Check if the slot overlaps with this break time + if not (current_time >= break_time.end_time or slot_end_time <= break_time.start_time): + conflict_with_break = True + print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_time.start_time}-{break_time.end_time}") + break if not conflict_with_break: # Add this slot to available slots diff --git a/recruitment/views.py b/recruitment/views.py index 95a2b40..9ab9d48 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -1,6 +1,7 @@ import json import requests from rich import print +from django.template.loader import render_to_string from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from django.http import JsonResponse @@ -10,19 +11,23 @@ from django.db.models import Q from django.urls import reverse from django.conf import settings from django.utils import timezone -from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm +from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm,BreakTimeFormSet from rest_framework import viewsets from django.contrib import messages from django.core.paginator import Paginator from .linkedin_service import LinkedInService -from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule -from .models import ZoomMeeting, Candidate, JobPosting from .serializers import JobPostingSerializer, CandidateSerializer from django.shortcuts import get_object_or_404, render, redirect from django.views.generic import CreateView,UpdateView,DetailView,ListView from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,schedule_interviews,get_available_time_slots from django.views.decorators.csrf import ensure_csrf_cookie +from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule,BreakTime, ZoomMeeting, Candidate, JobPosting import logging +from datastar_py.django import ( + DatastarResponse, + ServerSentEventGenerator as SSE, + read_signals, +) logger=logging.getLogger(__name__) @@ -194,7 +199,6 @@ def create_job(request): - def edit_job(request,slug): """Edit an existing job posting""" if request.method=='POST': @@ -575,14 +579,8 @@ def applicant_job_detail(request,slug): # submissions = form.submissions.all().order_by('-submitted_at') # # Pagination -# paginator = Paginator(submissions, 20) -# page_number = request.GET.get('page') -# page_obj = paginator.get_page(page_number) +# -# return render(request, 'forms/form_submissions.html', { -# 'form': form, -# 'page_obj': page_obj -# }) @ensure_csrf_cookie @@ -732,9 +730,8 @@ def create_form_template(request): template = form.save(commit=False) template.created_by = request.user template.save() - messages.success(request, f'Form template "{template.name}" created successfully!') - return redirect('form_builder', template_id=template.id) + return redirect('form_templates_list') else: form = FormTemplateForm() @@ -770,15 +767,19 @@ def submit_form(request, template_id): """Handle form submission""" try: template = get_object_or_404(FormTemplate, id=template_id) - print(template) - # Create form submission - submission = FormSubmission.objects.create( - template=template, - applicant_name=request.POST.get('applicant_name', ''), - applicant_email=request.POST.get('applicant_email', '') - ) + # # Create form submission + # print({key: value for key, value in request.POST.items()}) + # first_name = next((value for key, value in request.POST.items() if key == 'First Name'), None) + # last_name = next((value for key, value in request.POST.items() if key == 'Last Name'), None) + # email = next((value for key, value in request.POST.items() if key == 'Email Address'), None) + # phone = next((value for key, value in request.POST.items() if key == 'Phone Number'), None) + # address = next((value for key, value in request.POST.items() if key == 'Address'), None) + # resume = next((value for key, value in request.POST.items() if key == 'Resume Upload'), None) + # print(first_name, last_name, email, phone, address, resume) + # create candidate + submission = FormSubmission.objects.create(template=template) # Process field responses for field_id, value in request.POST.items(): if field_id.startswith('field_'): @@ -806,7 +807,29 @@ def submit_form(request, template_id): ) except FormField.DoesNotExist: continue + try: + first_name = submission.responses.get(field__label="First Name") + last_name = submission.responses.get(field__label="Last Name") + email = submission.responses.get(field__label="Email Address") + phone = submission.responses.get(field__label="Phone Number") + address = submission.responses.get(field__label="Address") + resume = submission.responses.get(field__label="Resume Upload") + submission.applicant_name = f"{first_name.display_value} {last_name.display_value}" + submission.applicant_email = email.display_value + submission.save() + Candidate.objects.create( + first_name=first_name.display_value, + last_name=last_name.display_value, + email=email.display_value, + phone=phone.display_value, + address=address.display_value, + resume=resume.get_file if resume.is_file else None, + job=submission.template.job + ) + except Exception as e: + logger.error(f"Candidate creation failed,{e}") + pass return JsonResponse({ 'success': True, 'message': 'Form submitted successfully!', @@ -818,16 +841,32 @@ def submit_form(request, template_id): 'error': str(e) }, status=400) -def form_submission_details(request, form_id, submission_id): +def form_template_submissions_list(request, template_slug): + """List all submissions for a specific form template""" + template = get_object_or_404(FormTemplate, slug=template_slug, created_by=request.user) + + submissions = FormSubmission.objects.filter(template=template).order_by('-submitted_at') + + # Pagination + paginator = Paginator(submissions, 10) # Show 10 submissions per page + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + return render(request, 'forms/form_template_submissions_list.html', { + 'template': template, + 'page_obj': page_obj + }) + +def form_submission_details(request, template_id, submission_id): """Display detailed view of a specific form submission""" # Get the form template and verify ownership - form = get_object_or_404(FormTemplate, id=form_id, created_by=request.user) + template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) # Get the specific submission - submission = get_object_or_404(FormSubmission, id=submission_id, template=form) + submission = get_object_or_404(FormSubmission, id=submission_id, template=template) # Get all stages with their fields - stages = form.stages.prefetch_related('fields').order_by('order') + stages = template.stages.prefetch_related('fields').order_by('order') # Get all responses for this submission, ordered by field order responses = submission.responses.select_related('field').order_by('field__order') @@ -839,22 +878,21 @@ def form_submission_details(request, form_id, submission_id): 'stage': stage, 'responses': responses.filter(field__stage=stage) } - # print(stages) + return render(request, 'forms/form_submission_details.html', { - 'form': form, + 'template': template, 'submission': submission, 'stages': stages, 'responses': responses, 'stage_responses': stage_responses }) - - -def schedule_interviews_view(request, slug): - job = get_object_or_404(JobPosting, slug=slug) +def schedule_interviews_view(request, job_id): + job = get_object_or_404(Job, id=job_id) if request.method == 'POST': - form = InterviewScheduleForm(slug, request.POST) + form = InterviewScheduleForm(job_id, request.POST) + break_formset = BreakTimeFormSet(request.POST) # Check if this is a confirmation request if 'confirm_schedule' in request.POST: @@ -862,7 +900,7 @@ def schedule_interviews_view(request, slug): schedule_data = request.session.get('interview_schedule_data') if not schedule_data: messages.error(request, "Session expired. Please try again.") - return redirect('schedule_interviews', slug=slug) + return redirect('schedule_interviews', job_id=job_id) # Create the interview schedule schedule = InterviewSchedule.objects.create( @@ -875,6 +913,15 @@ def schedule_interviews_view(request, slug): candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids']) schedule.candidates.set(candidates) + # Add break times to the schedule + if 'breaks' in schedule_data and schedule_data['breaks']: + for break_data in schedule_data['breaks']: + break_time = BreakTime.objects.create( + start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(), + end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time() + ) + schedule.breaks.add(break_time) + # Schedule the interviews try: scheduled_count = schedule_interviews(schedule) @@ -885,16 +932,16 @@ def schedule_interviews_view(request, slug): # Clear the session data if 'interview_schedule_data' in request.session: del request.session['interview_schedule_data'] - return redirect('job_detail', slug=slug) + return redirect('job_detail', pk=job_id) except Exception as e: messages.error( request, f"Error scheduling interviews: {str(e)}" ) - return redirect('schedule_interviews', slug=slug) + return redirect('schedule_interviews', job_id=job_id) # This is the initial form submission - if form.is_valid(): + if form.is_valid() and break_formset.is_valid(): # Get the form data candidates = form.cleaned_data['candidates'] start_date = form.cleaned_data['start_date'] @@ -902,11 +949,18 @@ def schedule_interviews_view(request, slug): working_days = form.cleaned_data['working_days'] start_time = form.cleaned_data['start_time'] end_time = form.cleaned_data['end_time'] - break_start_time = form.cleaned_data['break_start_time'] - break_end_time = form.cleaned_data['break_end_time'] interview_duration = form.cleaned_data['interview_duration'] buffer_time = form.cleaned_data['buffer_time'] + # Process break times + breaks = [] + for break_form in break_formset: + if break_form.cleaned_data and not break_form.cleaned_data.get('DELETE'): + breaks.append({ + 'start_time': break_form.cleaned_data['start_time'].isoformat(), + 'end_time': break_form.cleaned_data['end_time'].isoformat() + }) + # Create a temporary schedule object (not saved to DB) temp_schedule = InterviewSchedule( job=job, @@ -915,14 +969,20 @@ def schedule_interviews_view(request, slug): working_days=working_days, start_time=start_time, end_time=end_time, - break_start_time=break_start_time, - break_end_time=break_end_time, interview_duration=interview_duration, buffer_time=buffer_time ) + # Create temporary break time objects + temp_breaks = [] + for break_data in breaks: + temp_breaks.append(BreakTime( + start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(), + end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time() + )) + # Get available slots - available_slots = get_available_time_slots(temp_schedule) + available_slots = get_available_time_slots(temp_schedule, temp_breaks) if len(available_slots) < len(candidates): messages.error( @@ -931,6 +991,7 @@ def schedule_interviews_view(request, slug): ) return render(request, 'interviews/schedule_interviews.html', { 'form': form, + 'break_formset': break_formset, 'job': job }) @@ -951,11 +1012,10 @@ def schedule_interviews_view(request, slug): 'working_days': working_days, 'start_time': start_time.isoformat(), 'end_time': end_time.isoformat(), - 'break_start_time': break_start_time.isoformat() if break_start_time else None, - 'break_end_time': break_end_time.isoformat() if break_end_time else None, 'interview_duration': interview_duration, 'buffer_time': buffer_time, - 'candidate_ids': [c.id for c in candidates] + 'candidate_ids': [c.id for c in candidates], + 'breaks': breaks } request.session['interview_schedule_data'] = schedule_data @@ -968,15 +1028,16 @@ def schedule_interviews_view(request, slug): 'working_days': working_days, 'start_time': start_time, 'end_time': end_time, - 'break_start_time': break_start_time, - 'break_end_time': break_end_time, + 'breaks': breaks, 'interview_duration': interview_duration, 'buffer_time': buffer_time }) else: - form = InterviewScheduleForm(slug=slug) + form = InterviewScheduleForm(job_id=job_id) + break_formset = BreakTimeFormSet() return render(request, 'interviews/schedule_interviews.html', { 'form': form, + 'break_formset': break_formset, 'job': job - }) \ No newline at end of file + }) diff --git a/templates/base.html b/templates/base.html index caf1cc4..010b2ab 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,5 @@ {% load i18n static %} +{% load partials %} @@ -99,7 +100,7 @@ background-color: var(--kaauh-light-bg); color: var(--kaauh-teal-dark); } - + /* Language Toggle Button Style */ .language-toggle-btn { color: white !important; @@ -470,20 +471,19 @@ +
- {% if messages %} - {% for message in messages %} - + {% if messages %} + {% for message in messages %} + {% endfor %} - {% endif %} - - {% block content %} - {% endblock %} -
- + {% endif %} + {% block content %} + {% endblock %} +