diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index 988431b..b611091 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-313.pyc and b/NorahUniversity/__pycache__/settings.cpython-313.pyc differ diff --git a/NorahUniversity/__pycache__/urls.cpython-313.pyc b/NorahUniversity/__pycache__/urls.cpython-313.pyc index 624686a..f3db1f1 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-313.pyc and b/NorahUniversity/__pycache__/urls.cpython-313.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 249db2c..baaf0bb 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -67,7 +67,7 @@ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = { } } - ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A' ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA' ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L' @@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' - LINKEDIN_CLIENT_ID = '867jwsiyem1504' LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==' LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' \ No newline at end of file diff --git a/recruitment/__pycache__/admin.cpython-313.pyc b/recruitment/__pycache__/admin.cpython-313.pyc index 6bb62fc..a4f7fc8 100644 Binary files a/recruitment/__pycache__/admin.cpython-313.pyc and b/recruitment/__pycache__/admin.cpython-313.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index ec10c53..ffffc9f 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..7de510d 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..530e3d0 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..f8eb656 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..13274c0 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..151d83e 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-313.pyc b/recruitment/__pycache__/views_frontend.cpython-313.pyc index 16dcc88..7a1340c 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-313.pyc and b/recruitment/__pycache__/views_frontend.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 6806366..8317b4b 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,20 +465,22 @@ 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 return [int(day) for day in working_days] - -class JobStatusUpdateForm(forms.ModelForm): - class Meta: - model=JobPosting - fields=[ - 'status' - ] \ No newline at end of file + +class JobPostingCancelReasonForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['cancel_reason'] +class JobPostingStatusForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['status'] +class FormTemplateIsActiveForm(forms.ModelForm): + class Meta: + model = FormTemplate + fields = ['is_active'] \ No newline at end of file diff --git a/recruitment/linkedin.py b/recruitment/linkedin.py index b67abde..dae3bfc 100644 --- a/recruitment/linkedin.py +++ b/recruitment/linkedin.py @@ -2,12 +2,13 @@ import requests LINKEDIN_API_BASE = "https://api.linkedin.com/v2" + class LinkedInService: def __init__(self, access_token): self.headers = { - 'Authorization': f'Bearer {access_token}', - 'X-Restli-Protocol-Version': '2.0.0', - 'Content-Type': 'application/json' + "Authorization": f"Bearer {access_token}", + "X-Restli-Protocol-Version": "2.0.0", + "Content-Type": "application/json", } def post_job(self, organization_id, job_data): @@ -17,10 +18,10 @@ class LinkedInService: "lifecycleState": "PUBLISHED", "specificContent": { "com.linkedin.ugc.ShareContent": { - "shareCommentary": {"text": job_data['text']}, - "shareMediaCategory": "NONE" + "shareCommentary": {"text": job_data["text"]}, + "shareMediaCategory": "NONE", } }, - "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"} + "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}, } - return requests.post(url, json=data, headers=self.headers) \ No newline at end of file + return requests.post(url, json=data, headers=self.headers) diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index fc8a860..27ae747 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-09 10:10 +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,388 @@ 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')), + ('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(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='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], ), + migrations.CreateModel( + name='SharedFormTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')), + ('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)), + ('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')), + ], + options={ + 'verbose_name': 'Shared Form Template', + 'verbose_name_plural': 'Shared Form Templates', + }, + ), + migrations.CreateModel( + name='IntegrationLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')), + ('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')), + ('method', models.CharField(blank=True, max_length=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')), + ('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)), + ('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')), + ], + options={ + 'abstract': False, + }, + ), ] diff --git a/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py b/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py new file mode 100644 index 0000000..91b6321 --- /dev/null +++ b/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.6 on 2025-10-09 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='cancel_reason', + field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'), + ), + migrations.AddField( + model_name='jobposting', + name='cancelled_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='jobposting', + name='cancelled_by', + field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'), + ), + migrations.AlterField( + model_name='jobposting', + name='status', + field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True), + ), + ] 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_is_resume_parsed_and_more.py b/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py new file mode 100644 index 0000000..959dd78 --- /dev/null +++ b/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.6 on 2025-10-09 12:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='candidate', + name='is_resume_parsed', + field=models.BooleanField(default=False, verbose_name='Resume Parsed'), + ), + migrations.AlterField( + model_name='formtemplate', + name='is_active', + field=models.BooleanField(default=False, help_text='Whether this template is active'), + ), + ] 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/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/0027_profile.py b/recruitment/migrations/0027_profile.py deleted file mode 100644 index d2b58c8..0000000 --- a/recruitment/migrations/0027_profile.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-08 13:01 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0026_interviewschedule_scheduledinterview'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Profile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc deleted file mode 100644 index a76b82e..0000000 Binary files a/recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 0a882a2..0000000 Binary files a/recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc index 72c4c4a..dfcea98 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__/0002_trainingmaterial.cpython-311.pyc b/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-311.pyc deleted file mode 100644 index 5cad7cd..0000000 Binary files a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-312.pyc b/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-312.pyc deleted file mode 100644 index a68d9e1..0000000 Binary files a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-313.pyc b/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-313.pyc deleted file mode 100644 index de3f533..0000000 Binary files a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-311.pyc b/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-311.pyc deleted file mode 100644 index 699d7a5..0000000 Binary files a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-312.pyc deleted file mode 100644 index 6b5a791..0000000 Binary files a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-313.pyc deleted file mode 100644 index f8739c4..0000000 Binary files a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-311.pyc b/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-311.pyc deleted file mode 100644 index 7843763..0000000 Binary files a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-312.pyc b/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-312.pyc deleted file mode 100644 index 5c2f438..0000000 Binary files a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-313.pyc b/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-313.pyc deleted file mode 100644 index 1403683..0000000 Binary files a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-312.pyc b/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-312.pyc deleted file mode 100644 index f5fcc8a..0000000 Binary files a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc b/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc deleted file mode 100644 index 4b39aa5..0000000 Binary files a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-312.pyc deleted file mode 100644 index ab445a7..0000000 Binary files a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-313.pyc deleted file mode 100644 index 8ba6794..0000000 Binary files a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-312.pyc b/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-312.pyc deleted file mode 100644 index 35501e4..0000000 Binary files a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-313.pyc b/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-313.pyc deleted file mode 100644 index 2a384b0..0000000 Binary files a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-312.pyc b/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-312.pyc deleted file mode 100644 index 3da5401..0000000 Binary files a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-313.pyc b/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-313.pyc deleted file mode 100644 index 8930ea6..0000000 Binary files a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-312.pyc deleted file mode 100644 index 594a752..0000000 Binary files a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-313.pyc deleted file mode 100644 index 06259de..0000000 Binary files a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-312.pyc b/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-312.pyc deleted file mode 100644 index fda42e8..0000000 Binary files a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-313.pyc b/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-313.pyc deleted file mode 100644 index fa50f31..0000000 Binary files a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-312.pyc b/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-312.pyc deleted file mode 100644 index b9294cd..0000000 Binary files a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-313.pyc b/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-313.pyc deleted file mode 100644 index 9536144..0000000 Binary files a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-312.pyc b/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-312.pyc deleted file mode 100644 index 372cae6..0000000 Binary files a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-313.pyc b/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-313.pyc deleted file mode 100644 index 25b7217..0000000 Binary files a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-312.pyc deleted file mode 100644 index 41656b9..0000000 Binary files a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-313.pyc deleted file mode 100644 index 9e12a7f..0000000 Binary files a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc deleted file mode 100644 index 4c63586..0000000 Binary files a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-313.pyc deleted file mode 100644 index e488061..0000000 Binary files a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-312.pyc b/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-312.pyc deleted file mode 100644 index d3c978a..0000000 Binary files a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-313.pyc b/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-313.pyc deleted file mode 100644 index 350f334..0000000 Binary files a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-312.pyc deleted file mode 100644 index 6ff14ba..0000000 Binary files a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-313.pyc deleted file mode 100644 index b1ea43d..0000000 Binary files a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc deleted file mode 100644 index a5f4837..0000000 Binary files a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-313.pyc deleted file mode 100644 index c7cd58e..0000000 Binary files a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc b/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc deleted file mode 100644 index 250d210..0000000 Binary files a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-313.pyc b/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-313.pyc deleted file mode 100644 index 4ec6bb9..0000000 Binary files a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc b/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc deleted file mode 100644 index a339fa6..0000000 Binary files a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-313.pyc b/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-313.pyc deleted file mode 100644 index be96779..0000000 Binary files a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc b/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc deleted file mode 100644 index 7591704..0000000 Binary files a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-313.pyc b/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-313.pyc deleted file mode 100644 index 180c2e8..0000000 Binary files a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0020_delete_job.cpython-313.pyc b/recruitment/migrations/__pycache__/0020_delete_job.cpython-313.pyc deleted file mode 100644 index b33ef20..0000000 Binary files a/recruitment/migrations/__pycache__/0020_delete_job.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0021_source_api_key_source_api_secret_source_description_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0021_source_api_key_source_api_secret_source_description_and_more.cpython-313.pyc deleted file mode 100644 index 53f32bd..0000000 Binary files a/recruitment/migrations/__pycache__/0021_source_api_key_source_api_secret_source_description_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0022_alter_source_trusted_ips.cpython-313.pyc b/recruitment/migrations/__pycache__/0022_alter_source_trusted_ips.cpython-313.pyc deleted file mode 100644 index 99727f1..0000000 Binary files a/recruitment/migrations/__pycache__/0022_alter_source_trusted_ips.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0023_alter_jobposting_application_url_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0023_alter_jobposting_application_url_and_more.cpython-313.pyc deleted file mode 100644 index fbfeec0..0000000 Binary files a/recruitment/migrations/__pycache__/0023_alter_jobposting_application_url_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0024_fieldresponse_created_at_fieldresponse_slug_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0024_fieldresponse_created_at_fieldresponse_slug_and_more.cpython-313.pyc deleted file mode 100644 index f596782..0000000 Binary files a/recruitment/migrations/__pycache__/0024_fieldresponse_created_at_fieldresponse_slug_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-311.pyc b/recruitment/migrations/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 6c581a1..0000000 Binary files a/recruitment/migrations/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-312.pyc b/recruitment/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index c4ac56c..0000000 Binary files a/recruitment/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-313.pyc b/recruitment/migrations/__pycache__/__init__.cpython-313.pyc index bde0d3d..b67d53b 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..4b3a97b 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -9,17 +9,23 @@ 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') + 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 +43,146 @@ 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"), + ("ACTIVE", "Active"), + ("CLOSED", "Closed"), + ("CANCELLED", "Cancelled"), + ("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" + ), ) + cancel_reason = models.TextField( + blank=True, + help_text=_("Reason for canceling the job posting"), + verbose_name=_("Cancel Reason"), + ) + cancelled_by = models.CharField( + max_length=100, + blank=True, + help_text=_("Name of person who cancelled this job"), + verbose_name=_("Cancelled By"), + ) + cancelled_at = models.DateTimeField(null=True, blank=True) class Meta: - ordering = ['-created_at'] + ordering = ["-created_at"] verbose_name = "Job Posting" verbose_name_plural = "Job Postings" @@ -133,24 +190,29 @@ 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 - self.internal_job_id = f"{prefix}-{year}-{next_num:04d}" + self.internal_job_id = f"{prefix}-{year}-{next_num:06d}" super().save(*args, **kwargs) @@ -161,9 +223,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 +234,88 @@ 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")) + is_resume_parsed = models.BooleanField( + default=False, verbose_name=_("Resume Parsed") + ) + parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary")) + applied = models.BooleanField(default=False, verbose_name=_("Applied")) + stage = models.CharField( + 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 +323,33 @@ 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 +358,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 +381,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,25 +390,37 @@ 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, []) + @property + def submission(self): + return FormSubmission.objects.filter(template__job=self.job).first() + @property + def responses(self): + if self.submission: + return self.submission.responses.all() + return [] def __str__(self): return self.full_name 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 +428,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 +464,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=False, 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 +494,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 +527,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 +600,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 @@ -458,21 +622,35 @@ class FormField(Base): if self.order < 0: raise ValidationError("Order must be a positive integer") + def __str__(self): + return f"{self.stage.template.name} - {self.stage.name} - {self.label}" + 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 +660,51 @@ 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 +719,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 +740,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 +764,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 +876,101 @@ 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..2c6edb1 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -24,15 +24,8 @@ 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: + if instance.is_resume_parsed: 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 @@ -117,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs): instance.weaknesses = result1.get('weaknesses', '') instance.criteria_checklist = result1.get('criteria_checklist', {}) - + instance.is_resume_parsed = True # Save only scoring-related fields to avoid recursion instance.save(update_fields=[ 'match_score', 'strengths', 'weaknesses', - 'criteria_checklist','parsed_summary' + 'criteria_checklist','parsed_summary', 'is_resume_parsed' ]) logger.info(f"Successfully scored resume for candidate {instance.id}") @@ -144,7 +137,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 +148,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 +175,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 +183,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 +191,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 ae6fead..b46f732 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -66,7 +66,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..d925ad8 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 @@ -534,4 +535,19 @@ def get_available_time_slots(schedule): current_date += timedelta(days=1) print(f"Total slots generated: {len(slots)}") - return slots \ No newline at end of file + return slots + + + +def json_to_markdown_table(data_list): + if not data_list: + return "" + + headers = data_list[0].keys() + markdown = "| " + " | ".join(headers) + " |\n" + markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n" + + for row in data_list: + values = [str(row.get(header, "")) for header in headers] + markdown += "| " + " | ".join(values) + " |\n" + return markdown \ No newline at end of file diff --git a/recruitment/views.py b/recruitment/views.py index 78dc2d7..82c536a 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,27 +11,55 @@ 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,JobStatusUpdateForm +from .forms import ( + ZoomMeetingForm, + JobPostingForm, + FormTemplateForm, + InterviewScheduleForm,JobStatusUpdateForm, + 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.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__) +logger = logging.getLogger(__name__) class JobPostingViewSet(viewsets.ModelViewSet): queryset = JobPosting.objects.all() serializer_class = JobPostingSerializer + class CandidateViewSet(viewsets.ModelViewSet): queryset = Candidate.objects.all() serializer_class = CandidateSerializer @@ -38,9 +67,9 @@ class CandidateViewSet(viewsets.ModelViewSet): class ZoomMeetingCreateView(CreateView): model = ZoomMeeting - template_name = 'meetings/create_meeting.html' + template_name = "meetings/create_meeting.html" form_class = ZoomMeetingForm - success_url = '/' + success_url = "/" def form_valid(self, form): instance = form.save(commit=False) @@ -48,82 +77,85 @@ class ZoomMeetingCreateView(CreateView): topic = instance.topic if instance.start_time < timezone.now(): messages.error(self.request, "Start time must be in the future.") - return redirect('/create-meeting/', status=400) + return redirect("/create-meeting/", status=400) start_time = instance.start_time.isoformat() + "Z" duration = instance.duration result = create_zoom_meeting(topic, start_time, duration) if result["status"] == "success": - instance.meeting_id = result['meeting_details']['meeting_id'] - instance.join_url = result['meeting_details']['join_url'] - instance.host_email = result['meeting_details']['host_email'] - instance.zoom_gateway_response = result['zoom_gateway_response'] + instance.meeting_id = result["meeting_details"]["meeting_id"] + instance.join_url = result["meeting_details"]["join_url"] + instance.host_email = result["meeting_details"]["host_email"] + instance.zoom_gateway_response = result["zoom_gateway_response"] instance.save() messages.success(self.request, result["message"]) - return redirect('/', status=201) + return redirect("/", status=201) else: messages.error(self.request, result["message"]) - return redirect('/', status=400) + return redirect("/", status=400) except Exception as e: - return redirect('/', status=500) + return redirect("/", status=500) + class ZoomMeetingListView(ListView): model = ZoomMeeting - template_name = 'meetings/list_meetings.html' - context_object_name = 'meetings' + template_name = "meetings/list_meetings.html" + context_object_name = "meetings" paginate_by = 10 def get_queryset(self): - queryset = super().get_queryset().order_by('-start_time') + queryset = super().get_queryset().order_by("-start_time") # Handle search - search_query = self.request.GET.get('search', '') + search_query = self.request.GET.get("search", "") if search_query: queryset = queryset.filter( - Q(topic__icontains=search_query) | - Q(meeting_id__icontains=search_query) + Q(topic__icontains=search_query) | Q(meeting_id__icontains=search_query) ) return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['search_query'] = self.request.GET.get('search', '') + context["search_query"] = self.request.GET.get("search", "") return context + class ZoomMeetingDetailsView(DetailView): model = ZoomMeeting - template_name = 'meetings/meeting_details.html' - context_object_name = 'meeting' + template_name = "meetings/meeting_details.html" + context_object_name = "meeting" + class ZoomMeetingUpdateView(UpdateView): model = ZoomMeeting form_class = ZoomMeetingForm - context_object_name = 'meeting' - template_name = 'meetings/update_meeting.html' - success_url = '/' + context_object_name = "meeting" + template_name = "meetings/update_meeting.html" + success_url = "/" def form_valid(self, form): instance = form.save(commit=False) updated_data = { - 'topic': instance.topic, - 'start_time': instance.start_time.isoformat() + "Z", - 'duration': instance.duration + "topic": instance.topic, + "start_time": instance.start_time.isoformat() + "Z", + "duration": instance.duration, } if instance.start_time < timezone.now(): messages.error(self.request, "Start time must be in the future.") - return redirect(f'/update-meeting/{instance.pk}/', status=400) + return redirect(f"/update-meeting/{instance.pk}/", status=400) result = update_zoom_meeting(instance.meeting_id, updated_data) if result["status"] == "success": instance.save() messages.success(self.request, result["message"]) - return redirect(reverse('meeting_details', kwargs={'pk': instance.pk})) + return redirect(reverse("meeting_details", kwargs={"pk": instance.pk})) else: messages.error(self.request, result["message"]) - return redirect(reverse('meeting_details', kwargs={'pk': instance.pk})) + return redirect(reverse("meeting_details", kwargs={"pk": instance.pk})) + def ZoomMeetingDeleteView(request, pk): meeting = get_object_or_404(ZoomMeeting, pk=pk) @@ -135,13 +167,13 @@ def ZoomMeetingDeleteView(request, pk): messages.success(request, result["message"]) else: messages.error(request, result["message"]) - return redirect('/') + return redirect("/") except Exception as e: messages.error(request, str(e)) - return redirect('/') + return redirect("/") -#Job Posting +# Job Posting # def job_list(request): # """Display the list of job postings order by creation date descending""" # jobs=JobPosting.objects.all().order_by('-created_at') @@ -149,7 +181,7 @@ def ZoomMeetingDeleteView(request, pk): # # Filter by status if provided # print(f"the request is: {request} ") # status=request.GET.get('status') -# print(f"DEBUG: Status filter received: {status}") +# print(f"DEBUG: Status filter received: {status}") # if status: # jobs=jobs.filter(status=status) @@ -165,62 +197,71 @@ def ZoomMeetingDeleteView(request, pk): def create_job(request): """Create a new job posting""" - - if request.method=='POST': - form=JobPostingForm(request.POST,is_anonymous_user=not request.user.is_authenticated) - #to check user is authenticated or not + if request.method == "POST": + form = JobPostingForm( + request.POST, is_anonymous_user=not request.user.is_authenticated + ) + # to check user is authenticated or not if form.is_valid(): try: - job=form.save(commit=False) + job = form.save(commit=False) if request.user.is_authenticated: - job.created_by=request.user.get_full_name() or request.user.username + job.created_by = ( + request.user.get_full_name() or request.user.username + ) else: - job.created_by=request.POST.get('created_by','').strip() + job.created_by = request.POST.get("created_by", "").strip() if not job.created_by: - job.created_by="University Administrator" + job.created_by = "University Administrator" job.save() - messages.success(request,f'Job "{job.title}" created successfully!') - return redirect('job_list') + messages.success(request, f'Job "{job.title}" created successfully!') + return redirect("job_list") except Exception as e: logger.error(f"Error creating job: {e}") - messages.error(request,f"Error creating job: {e}") + messages.error(request, f"Error creating job: {e}") else: - messages.error(request, f'Please correct the errors below.{form.errors}') + messages.error(request, f"Please correct the errors below.{form.errors}") else: - form=JobPostingForm(is_anonymous_user=not request.user.is_authenticated) - return render(request,'jobs/create_job.html',{'form':form}) + form = JobPostingForm(is_anonymous_user=not request.user.is_authenticated) + return render(request, "jobs/create_job.html", {"form": form}) - - - -def edit_job(request,slug): +def edit_job(request, slug): """Edit an existing job posting""" - if request.method=='POST': - job=get_object_or_404(JobPosting,slug=slug) - form=JobPostingForm(request.POST,instance=job,is_anonymous_user=not request.user.is_authenticated) + if request.method == "POST": + job = get_object_or_404(JobPosting, slug=slug) + form = JobPostingForm( + request.POST, + instance=job, + is_anonymous_user=not request.user.is_authenticated, + ) if form.is_valid(): try: - job=form.save(commit=False) + job = form.save(commit=False) if request.user.is_authenticated: - job.created_by=request.user.get_full_name() or request.user.username + job.created_by = ( + request.user.get_full_name() or request.user.username + ) else: - job.created_by=request.POST.get('created_by','').strip() + job.created_by = request.POST.get("created_by", "").strip() if not job.created_by: - job.created_by="University Administrator" + job.created_by = "University Administrator" job.save() - messages.success(request,f'Job "{job.title}" updated successfully!') - return redirect('job_list') + messages.success(request, f'Job "{job.title}" updated successfully!') + return redirect("job_list") except Exception as e: logger.error(f"Error updating job: {e}") - messages.error(request,f"Error updating job: {e}") + messages.error(request, f"Error updating job: {e}") else: - messages.error(request, 'Please correct the errors below.') + messages.error(request, "Please correct the errors below.") else: - job=get_object_or_404(JobPosting,slug=slug) - form=JobPostingForm(instance=job,is_anonymous_user=not request.user.is_authenticated) - return render(request,'jobs/edit_job.html',{'form':form,'job':job}) + job = get_object_or_404(JobPosting, slug=slug) + form = JobPostingForm( + instance=job, is_anonymous_user=not request.user.is_authenticated + ) + return render(request, "jobs/edit_job.html", {"form": form, "job": job}) + def job_detail(request, slug): """View details of a specific job""" @@ -228,13 +269,13 @@ def job_detail(request, slug): # Get all candidates for this job, ordered by most recent - candidates = job.candidates.all().order_by('-created_at') + candidates = job.candidates.all().order_by("-created_at") # Count candidates by stage for summary statistics total_candidates = candidates.count() - applied_count = candidates.filter(stage='Applied').count() - interview_count = candidates.filter(stage='Interview').count() - offer_count = candidates.filter(stage='Offer').count() + applied_count = candidates.filter(stage="Applied").count() + interview_count = candidates.filter(stage="Interview").count() + offer_count = candidates.filter(stage="Offer").count() status_form = JobStatusUpdateForm(instance=job) @@ -258,78 +299,80 @@ def job_detail(request, slug): context = { - 'job': job, - 'candidates': candidates, - 'total_candidates': total_candidates, - 'applied_count': applied_count, - 'interview_count': interview_count, - 'offer_count': offer_count, + "job": job, + "candidates": candidates, + "total_candidates": total_candidates, + "applied_count": applied_count, + "interview_count": interview_count, + "offer_count": offer_count, 'status_form':status_form } - return render(request, 'jobs/job_detail.html', context) + return render(request, "jobs/job_detail.html", context) # job detail facing the candidate: -def job_detail_candidate(request,slug): - job=get_object_or_404(JobPosting,slug=slug) - return render(request,'jobs/job_detail_candidate.html',{'job':job}) +def job_detail_candidate(request, slug): + job = get_object_or_404(JobPosting, slug=slug) + return render(request, "jobs/job_detail_candidate.html", {"job": job}) -def post_to_linkedin(request,slug): + +def post_to_linkedin(request, slug): """Post a job to LinkedIn""" - job=get_object_or_404(JobPosting,slug=slug) - if job.status!='ACTIVE': - messages.info(request,'Only active jobs can be posted to LinkedIn.') - return redirect('job_list') + job = get_object_or_404(JobPosting, slug=slug) + if job.status != "ACTIVE": + messages.info(request, "Only active jobs can be posted to LinkedIn.") + return redirect("job_list") - if request.method=='POST': + if request.method == "POST": try: # Check if user is authenticated with LinkedIn - if 'linkedin_access_token' not in request.session: - messages.error(request,'Please authenticate with LinkedIn first.') - return redirect('linkedin_login') + if "linkedin_access_token" not in request.session: + messages.error(request, "Please authenticate with LinkedIn first.") + return redirect("linkedin_login") # Clear previous LinkedIn data for re-posting - job.posted_to_linkedin=False - job.linkedin_post_id='' - job.linkedin_post_url='' - job.linkedin_post_status='' - job.linkedin_posted_at=None + job.posted_to_linkedin = False + job.linkedin_post_id = "" + job.linkedin_post_url = "" + job.linkedin_post_status = "" + job.linkedin_posted_at = None job.save() # Initialize LinkedIn service - service=LinkedInService() - service.access_token=request.session['linkedin_access_token'] + service = LinkedInService() + service.access_token = request.session["linkedin_access_token"] # Post to LinkedIn - result=service.create_job_post(job) - if result['success']: + result = service.create_job_post(job) + if result["success"]: # Update job with LinkedIn info - job.posted_to_linkedin=True - job.linkedin_post_id=result['post_id'] - job.linkedin_post_url=result['post_url'] - job.linkedin_post_status='SUCCESS' - job.linkedin_posted_at=timezone.now() + job.posted_to_linkedin = True + job.linkedin_post_id = result["post_id"] + job.linkedin_post_url = result["post_url"] + job.linkedin_post_status = "SUCCESS" + job.linkedin_posted_at = timezone.now() job.save() - messages.success(request,'Job posted to LinkedIn successfully!') + messages.success(request, "Job posted to LinkedIn successfully!") else: - error_msg=result.get('error','Unknown error') - job.linkedin_post_status=f'ERROR: {error_msg}' + error_msg = result.get("error", "Unknown error") + job.linkedin_post_status = f"ERROR: {error_msg}" job.save() - messages.error(request,f'Error posting to LinkedIn: {error_msg}') + messages.error(request, f"Error posting to LinkedIn: {error_msg}") except Exception as e: logger.error(f"Error in post_to_linkedin: {e}") - job.linkedin_post_status = f'ERROR: {str(e)}' + job.linkedin_post_status = f"ERROR: {str(e)}" job.save() - messages.error(request, f'Error posting to LinkedIn: {e}') + messages.error(request, f"Error posting to LinkedIn: {e}") + + return redirect("job_detail", slug=job.slug) - return redirect('job_detail', slug=job.slug) def linkedin_login(request): """Redirect to LinkedIn OAuth""" - service=LinkedInService() - auth_url=service.get_auth_url() + service = LinkedInService() + auth_url = service.get_auth_url() """ It creates a special URL that: Sends the user to LinkedIn to log in @@ -342,28 +385,28 @@ def linkedin_login(request): def linkedin_callback(request): """Handle LinkedIn OAuth callback""" - code=request.GET.get('code') + code = request.GET.get("code") if not code: - messages.error(request,'No authorization code received from LinkedIn.') - return redirect('job_list') + messages.error(request, "No authorization code received from LinkedIn.") + return redirect("job_list") try: - service=LinkedInService() - #get_access_token(code)->It makes a POST request to LinkedIn’s token endpoint with parameters - access_token=service.get_access_token(code) - request.session['linkedin_access_token']=access_token - request.session['linkedin_authenticated']=True + service = LinkedInService() + # get_access_token(code)->It makes a POST request to LinkedIn’s token endpoint with parameters + access_token = service.get_access_token(code) + request.session["linkedin_access_token"] = access_token + request.session["linkedin_authenticated"] = True settings.LINKEDIN_IS_CONNECTED = True - messages.success(request,'Successfully authenticated with LinkedIn!') + messages.success(request, "Successfully authenticated with LinkedIn!") except Exception as e: logger.error(f"LinkedIn authentication error: {e}") - messages.error(request,f'LinkedIn authentication failed: {e}') + messages.error(request, f"LinkedIn authentication failed: {e}") - return redirect('job_list') + return redirect("job_list") -#applicant views -def applicant_job_detail(request,slug): +# applicant views +def applicant_job_detail(request, slug): """View job details for applicants""" job=get_object_or_404(JobPosting,slug=slug,status='ACTIVE') return render(request,'jobs/applicant_job_detail.html',{'job':job}) @@ -605,14 +648,7 @@ def application_success(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 @@ -620,10 +656,12 @@ def form_builder(request, template_id=None): """Render the form builder interface""" context = {} if template_id: - template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) - context['template_id'] = template.id - context['template_name'] = template.name - return render(request,'forms/form_builder.html',context) + template = get_object_or_404( + FormTemplate, id=template_id, created_by=request.user + ) + context["template_id"] = template.id + context["template_name"] = template.name + return render(request, "forms/form_builder.html", context) @csrf_exempt @@ -632,13 +670,15 @@ def save_form_template(request): """Save a new or existing form template""" try: data = json.loads(request.body) - template_name = data.get('name', 'Untitled Form') - stages_data = data.get('stages', []) - template_id = data.get('template_id') + template_name = data.get("name", "Untitled Form") + stages_data = data.get("stages", []) + template_id = data.get("template_id") if template_id: # Update existing template - template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) + template = get_object_or_404( + FormTemplate, id=template_id, created_by=request.user + ) template.name = template_name template.save() # Clear existing stages and fields @@ -646,50 +686,48 @@ def save_form_template(request): else: # Create new template template = FormTemplate.objects.create( - name=template_name, - created_by=request.user + name=template_name, created_by=request.user ) # Create stages and fields for stage_order, stage_data in enumerate(stages_data): stage = FormStage.objects.create( template=template, - name=stage_data['name'], + name=stage_data["name"], order=stage_order, - is_predefined=stage_data.get('predefined', False) + is_predefined=stage_data.get("predefined", False), ) - for field_order, field_data in enumerate(stage_data['fields']): - options = field_data.get('options', []) + for field_order, field_data in enumerate(stage_data["fields"]): + options = field_data.get("options", []) if not isinstance(options, list): options = [] - file_types = field_data.get('fileTypes', '') - max_file_size = field_data.get('maxFileSize', 5) + file_types = field_data.get("fileTypes", "") + max_file_size = field_data.get("maxFileSize", 5) FormField.objects.create( stage=stage, - label=field_data.get('label', ''), - field_type=field_data.get('type', 'text'), - placeholder=field_data.get('placeholder', ''), - required=field_data.get('required', False), + label=field_data.get("label", ""), + field_type=field_data.get("type", "text"), + placeholder=field_data.get("placeholder", ""), + required=field_data.get("required", False), order=field_order, - is_predefined=field_data.get('predefined', False), + is_predefined=field_data.get("predefined", False), options=options, file_types=file_types, - max_file_size=max_file_size + max_file_size=max_file_size, ) - return JsonResponse({ - 'success': True, - 'template_id': template.id, - 'message': 'Form template saved successfully!' - }) + return JsonResponse( + { + "success": True, + "template_id": template.id, + "message": "Form template saved successfully!", + } + ) except Exception as e: - return JsonResponse({ - 'success': False, - 'error': str(e) - }, status=400) + return JsonResponse({"success": False, "error": str(e)}, status=400) @require_http_methods(["GET"]) @@ -701,38 +739,46 @@ def load_form_template(request, template_id): for stage in template.stages.all(): fields = [] for field in stage.fields.all(): - fields.append({ - 'id': field.id, - 'type': field.field_type, - 'label': field.label, - 'placeholder': field.placeholder, - 'required': field.required, - 'options': field.options, - 'fileTypes': field.file_types, - 'maxFileSize': field.max_file_size, - 'predefined': field.is_predefined - }) - stages.append({ - 'id': stage.id, - 'name': stage.name, - 'predefined': stage.is_predefined, - 'fields': fields - }) + fields.append( + { + "id": field.id, + "type": field.field_type, + "label": field.label, + "placeholder": field.placeholder, + "required": field.required, + "options": field.options, + "fileTypes": field.file_types, + "maxFileSize": field.max_file_size, + "predefined": field.is_predefined, + } + ) + stages.append( + { + "id": stage.id, + "name": stage.name, + "predefined": stage.is_predefined, + "fields": fields, + } + ) - return JsonResponse({ - 'success': True, - 'template': { - 'id': template.id, - 'name': template.name, - 'description': template.description, - 'is_active': template.is_active, - 'job': template.job_id if template.job else None, - 'stages': stages + return JsonResponse( + { + "success": True, + "template": { + "id": template.id, + "name": template.name, + "description": template.description, + "is_active": template.is_active, + "job": template.job_id if template.job else None, + "stages": stages, + }, } - }) + ) + + def form_templates_list(request): """List all form templates for the current user""" - query = request.GET.get('q', '') + query = request.GET.get("q", "") templates = FormTemplate.objects.filter(created_by=request.user) if query: @@ -740,202 +786,289 @@ def form_templates_list(request): Q(name__icontains=query) | Q(description__icontains=query) ) - templates = templates.order_by('-created_at') + templates = templates.order_by("-created_at") paginator = Paginator(templates, 10) # Show 10 templates per page - page_number = request.GET.get('page') + page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) form = FormTemplateForm() - form.fields['job'].queryset = JobPosting.objects.filter(form_template__isnull=True) - context = { - 'templates': page_obj, - 'query': query, - 'form': form - } - return render(request, 'forms/form_templates_list.html', context) + form.fields["job"].queryset = JobPosting.objects.filter(form_template__isnull=True) + context = {"templates": page_obj, "query": query, "form": form} + return render(request, "forms/form_templates_list.html", context) def create_form_template(request): """Create a new form template""" - if request.method == 'POST': + if request.method == "POST": form = FormTemplateForm(request.POST) if form.is_valid(): 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) + messages.success( + request, f'Form template "{template.name}" created successfully!' + ) + return redirect("form_templates_list") else: form = FormTemplateForm() - return render(request, 'forms/create_form_template.html', {'form': form}) + return render(request, "forms/create_form_template.html", {"form": form}) + @require_http_methods(["GET"]) def list_form_templates(request): """List all form templates for the current user""" templates = FormTemplate.objects.filter(created_by=request.user).values( - 'id', 'name', 'description', 'created_at', 'updated_at' + "id", "name", "description", "created_at", "updated_at" ) - return JsonResponse({ - 'success': True, - 'templates': list(templates) - }) + return JsonResponse({"success": True, "templates": list(templates)}) + @require_http_methods(["DELETE"]) def delete_form_template(request, template_id): """Delete a form template""" template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) template.delete() - return JsonResponse({'success': True, 'message': 'Form template deleted successfully!'}) - + return JsonResponse( + {"success": True, "message": "Form template deleted successfully!"} + ) def form_wizard_view(request, template_id): """Display the form as a step-by-step wizard""" template = get_object_or_404(FormTemplate, id=template_id, is_active=True) - job_id=template.job.internal_job_id - return render(request, 'forms/form_wizard.html', {'template_id': template_id,'job_id':job_id}) -@require_http_methods(["POST"]) + job_id = template.job.internal_job_id + return render( + request, + "forms/form_wizard.html", + {"template_id": template_id, "job_id": job_id}, + ) + + +@require_http_methods(["GET", "POST"]) def submit_form(request, template_id): """Handle form submission""" - try: - template = get_object_or_404(FormTemplate, id=template_id) - print(template) + print("request method", request.method) + if request.method == "POST": + try: + template = get_object_or_404(FormTemplate, id=template_id) - # 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_"): + actual_field_id = field_id.replace("field_", "") + try: + field = FormField.objects.get( + id=actual_field_id, stage__template=template + ) + FieldResponse.objects.create( + submission=submission, + field=field, + value=value if value else None, + ) + except FormField.DoesNotExist: + continue + + # Handle file uploads + for field_id, uploaded_file in request.FILES.items(): + if field_id.startswith("field_"): + actual_field_id = field_id.replace("field_", "") + try: + field = FormField.objects.get( + id=actual_field_id, stage__template=template + ) + FieldResponse.objects.create( + submission=submission, + field=field, + uploaded_file=uploaded_file, + ) + 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!", + "submission_id": submission.id, + } + ) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}, status=400) + else: + # Handle GET request - this should not happen for form submission + return JsonResponse( + {"success": False, "error": "GET method not allowed for form submission"}, + status=405, ) - # Process field responses - for field_id, value in request.POST.items(): - if field_id.startswith('field_'): - actual_field_id = field_id.replace('field_', '') - try: - field = FormField.objects.get(id=actual_field_id, stage__template=template) - FieldResponse.objects.create( - submission=submission, - field=field, - value=value if value else None - ) - except FormField.DoesNotExist: - continue - # Handle file uploads - for field_id, uploaded_file in request.FILES.items(): - if field_id.startswith('field_'): - actual_field_id = field_id.replace('field_', '') - try: - field = FormField.objects.get(id=actual_field_id, stage__template=template) - FieldResponse.objects.create( - submission=submission, - field=field, - uploaded_file=uploaded_file - ) - except FormField.DoesNotExist: - continue +def form_template_submissions_list(request, slug): + """List all submissions for a specific form template""" + template = get_object_or_404(FormTemplate, slug=slug) - return JsonResponse({ - 'success': True, - 'message': 'Form submitted successfully!', - 'submission_id': submission.id - }) - except Exception as e: - return JsonResponse({ - 'success': False, - 'error': str(e) - }, status=400) + submissions = FormSubmission.objects.filter(template=template).order_by( + "-submitted_at" + ) -def form_submission_details(request, form_id, submission_id): + # 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') + responses = submission.responses.select_related("field").order_by("field__order") # Group responses by stage stage_responses = {} for stage in stages: stage_responses[stage.id] = { - 'stage': stage, - 'responses': responses.filter(field__stage=stage) + "stage": stage, + "responses": responses.filter(field__stage=stage), } - # print(stages) - return render(request, 'forms/form_submission_details.html', { - 'form': form, - 'submission': submission, - 'stages': stages, - 'responses': responses, - 'stage_responses': stage_responses - }) + + return render( + request, + "forms/form_submission_details.html", + { + "template": template, + "submission": submission, + "stages": stages, + "responses": responses, + "stage_responses": stage_responses, + }, + ) +def schedule_interviews_view(request, job_id): + job = get_object_or_404(JobPosting, id=job_id) -def schedule_interviews_view(request, slug): - job = get_object_or_404(JobPosting, slug=slug) - - if request.method == 'POST': - form = InterviewScheduleForm(slug, request.POST) + if request.method == "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: + if "confirm_schedule" in request.POST: # Get the schedule data from session - schedule_data = request.session.get('interview_schedule_data') + 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( - job=job, - created_by=request.user, - **schedule_data + job=job, created_by=request.user, **schedule_data ) # Add candidates to the schedule - candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids']) + 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) messages.success( - request, - f"Successfully scheduled {scheduled_count} interviews." + request, f"Successfully scheduled {scheduled_count} interviews." ) # Clear the session data - if 'interview_schedule_data' in request.session: - del request.session['interview_schedule_data'] - return redirect('job_detail', slug=slug) + if "interview_schedule_data" in request.session: + del request.session["interview_schedule_data"] + 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) + messages.error(request, f"Error scheduling interviews: {str(e)}") + 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'] - end_date = form.cleaned_data['end_date'] - 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'] + candidates = form.cleaned_data["candidates"] + start_date = form.cleaned_data["start_date"] + end_date = form.cleaned_data["end_date"] + working_days = form.cleaned_data["working_days"] + start_time = form.cleaned_data["start_time"] + end_time = form.cleaned_data["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( @@ -945,68 +1078,83 @@ 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 + 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( request, - f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}" + f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}", + ) + return render( + request, + "interviews/schedule_interviews.html", + {"form": form, "break_formset": break_formset, "job": job}, ) - return render(request, 'interviews/schedule_interviews.html', { - 'form': form, - 'job': job - }) # Create a preview schedule preview_schedule = [] for i, candidate in enumerate(candidates): slot = available_slots[i] - preview_schedule.append({ - 'candidate': candidate, - 'date': slot['date'], - 'time': slot['time'] - }) + preview_schedule.append( + {"candidate": candidate, "date": slot["date"], "time": slot["time"]} + ) # Save the form data to session for later use schedule_data = { - 'start_date': start_date.isoformat(), - 'end_date': end_date.isoformat(), - '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] + "start_date": start_date.isoformat(), + "end_date": end_date.isoformat(), + "working_days": working_days, + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "interview_duration": interview_duration, + "buffer_time": buffer_time, + "candidate_ids": [c.id for c in candidates], + "breaks": breaks, } - request.session['interview_schedule_data'] = schedule_data + request.session["interview_schedule_data"] = schedule_data # Render the preview page - return render(request, 'interviews/preview_schedule.html', { - 'job': job, - 'schedule': preview_schedule, - 'start_date': start_date, - 'end_date': end_date, - '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 - }) + return render( + request, + "interviews/preview_schedule.html", + { + "job": job, + "schedule": preview_schedule, + "start_date": start_date, + "end_date": end_date, + "working_days": working_days, + "start_time": start_time, + "end_time": 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, - 'job': job - }) \ No newline at end of file + return render( + request, + "interviews/schedule_interviews.html", + {"form": form, "break_formset": break_formset, "job": job}, + ) diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index 54a79d2..7451276 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -1,6 +1,9 @@ +import json from django.shortcuts import render, get_object_or_404,redirect from django.contrib import messages from django.http import JsonResponse + +from recruitment.utils import json_to_markdown_table from . import models from django.utils.translation import get_language from . import forms @@ -20,6 +23,8 @@ from datastar_py.django import ( ServerSentEventGenerator as SSE, read_signals, ) +# from rich import print +from rich.markdown import CodeBlock class JobListView(LoginRequiredMixin, ListView): model = models.JobPosting @@ -42,7 +47,7 @@ class JobListView(LoginRequiredMixin, ListView): # Filter for non-staff users if not self.request.user.is_staff: queryset = queryset.filter(status='Published') - + status=self.request.GET.get('status') if status: queryset=queryset.filter(status=status) @@ -50,7 +55,7 @@ class JobListView(LoginRequiredMixin, ListView): return queryset def get_context_data(self, **kwargs): - + context = super().get_context_data(**kwargs) context['search_query'] = self.request.GET.get('search', '') context['lang'] = get_language() @@ -205,6 +210,7 @@ def training_list(request): def candidate_detail(request, slug): + from rich.json import JSON candidate = get_object_or_404(models.Candidate, slug=slug) try: parsed = ast.literal_eval(candidate.parsed_summary) @@ -216,6 +222,8 @@ def candidate_detail(request, slug): if request.user.is_staff: stage_form = forms.CandidateStageForm(candidate=candidate) + # parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False) + parsed = json_to_markdown_table([parsed]) return render(request, 'recruitment/candidate_detail.html', { 'candidate': candidate, 'parsed': parsed, @@ -223,7 +231,7 @@ def candidate_detail(request, slug): }) def candidate_update_stage(request, slug): - """Handle HTMX stage update requests""" + """Handle HTMX stage update requests""" try: if not request.user.is_staff: return render(request, 'recruitment/partials/error.html', {'error': 'Permission denied'}, status=403) @@ -297,7 +305,7 @@ class TrainingListView(LoginRequiredMixin, ListView): template_name = 'recruitment/training_list.html' context_object_name = 'materials' paginate_by = 10 - + def get_queryset(self): queryset = super().get_queryset() 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 %} +