more update related to integrations
This commit is contained in:
parent
41cf8ea28a
commit
579cc085e2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,24 +1,24 @@
|
||||
from django import forms
|
||||
from .validators import validate_hash_tags
|
||||
from django.core.validators import URLValidator
|
||||
from django.forms.formsets import formset_factory
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
|
||||
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule
|
||||
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule,BreakTime
|
||||
from django_summernote.widgets import SummernoteWidget
|
||||
|
||||
|
||||
class CandidateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Candidate
|
||||
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume', 'stage']
|
||||
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume',]
|
||||
labels = {
|
||||
'first_name': _('First Name'),
|
||||
'last_name': _('Last Name'),
|
||||
'phone': _('Phone'),
|
||||
'email': _('Email'),
|
||||
'resume': _('Resume'),
|
||||
'stage': _('Application Stage'),
|
||||
}
|
||||
widgets = {
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter first name')}),
|
||||
@ -154,17 +154,17 @@ class TrainingMaterialForm(forms.ModelForm):
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter material title')}),
|
||||
# 💡 Use SummernoteWidget here
|
||||
'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}),
|
||||
'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}),
|
||||
'video_link': forms.URLInput(attrs={'class': 'form-control', 'placeholder': _('https://www.youtube.com/watch?v=...')}),
|
||||
'file': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
|
||||
# The __init__ and FormHelper layout remains the same
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_class = 'g-3'
|
||||
self.helper.form_class = 'g-3'
|
||||
|
||||
self.helper.layout = Layout(
|
||||
'title',
|
||||
@ -175,7 +175,7 @@ class TrainingMaterialForm(forms.ModelForm):
|
||||
css_class='g-3 mb-4'
|
||||
),
|
||||
Div(
|
||||
Submit('submit', _('Create Material'),
|
||||
Submit('submit', _('Create Material'),
|
||||
css_class='btn btn-main-action'),
|
||||
css_class='col-12 mt-4'
|
||||
)
|
||||
@ -261,7 +261,7 @@ class JobPostingForm(forms.ModelForm):
|
||||
'application_instructions': SummernoteWidget(attrs={
|
||||
|
||||
'placeholder': 'Special instructions for applicants (e.g., required documents, reference requirements, etc.)',
|
||||
|
||||
|
||||
}),
|
||||
'open_positions': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
@ -412,11 +412,21 @@ class FormTemplateForm(forms.ModelForm):
|
||||
Field('is_active', css_class='form-check-input'),
|
||||
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
|
||||
)
|
||||
class BreakTimeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BreakTime
|
||||
fields = ['start_time', 'end_time']
|
||||
widgets = {
|
||||
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
}
|
||||
|
||||
BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
|
||||
|
||||
class InterviewScheduleForm(forms.ModelForm):
|
||||
candidates = forms.ModelMultipleChoiceField(
|
||||
queryset=Candidate.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True
|
||||
)
|
||||
working_days = forms.MultipleChoiceField(
|
||||
@ -429,24 +439,23 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
(5, 'Saturday'),
|
||||
(6, 'Sunday'),
|
||||
],
|
||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterviewSchedule
|
||||
fields = [
|
||||
'candidates', 'start_date', 'end_date', 'working_days',
|
||||
'start_time', 'end_time', 'break_start_time', 'break_end_time',
|
||||
'interview_duration', 'buffer_time'
|
||||
'start_time', 'end_time', 'interview_duration', 'buffer_time'
|
||||
]
|
||||
widgets = {
|
||||
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
'break_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
def __init__(self, slug, *args, **kwargs):
|
||||
@ -456,11 +465,7 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
job__slug=slug,
|
||||
stage='Interview'
|
||||
)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_class = 'form-horizontal'
|
||||
self.helper.label_class = 'col-md-3'
|
||||
self.helper.field_class = 'col-md-9'
|
||||
|
||||
def clean_working_days(self):
|
||||
working_days = self.cleaned_data.get('working_days')
|
||||
# Convert string values to integers
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
# Generated by Django 5.2.1 on 2025-05-18 17:23
|
||||
# Generated by Django 5.2.6 on 2025-10-08 15:48
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
import django_extensions.db.fields
|
||||
import recruitment.validators
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@ -9,32 +14,375 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Job',
|
||||
name='BreakTime',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('description_en', models.TextField()),
|
||||
('description_ar', models.TextField()),
|
||||
('is_published', models.BooleanField(default=False)),
|
||||
('posted_to_linkedin', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormStage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='Name of the stage', max_length=200)),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Stage',
|
||||
'verbose_name_plural': 'Form Stages',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HiringAgency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
|
||||
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('website', models.URLField(blank=True)),
|
||||
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
|
||||
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
|
||||
('address', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Hiring Agency',
|
||||
'verbose_name_plural': 'Hiring Agencies',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Source',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')),
|
||||
('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')),
|
||||
('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')),
|
||||
('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')),
|
||||
('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')),
|
||||
('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')),
|
||||
('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')),
|
||||
('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Source',
|
||||
'verbose_name_plural': 'Sources',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ZoomMeeting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('topic', models.CharField(max_length=255, verbose_name='Topic')),
|
||||
('meeting_id', models.CharField(max_length=20, unique=True, verbose_name='Meeting ID')),
|
||||
('start_time', models.DateTimeField(verbose_name='Start Time')),
|
||||
('duration', models.PositiveIntegerField(verbose_name='Duration')),
|
||||
('timezone', models.CharField(max_length=50, verbose_name='Timezone')),
|
||||
('join_url', models.URLField(verbose_name='Join URL')),
|
||||
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
|
||||
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
|
||||
('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')),
|
||||
('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')),
|
||||
('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormField',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('label', models.CharField(help_text='Label for the field', max_length=200)),
|
||||
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
|
||||
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
|
||||
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
|
||||
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
|
||||
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
|
||||
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
|
||||
('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')),
|
||||
('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')),
|
||||
('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Field',
|
||||
'verbose_name_plural': 'Form Fields',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormSubmission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('submitted_at', models.DateTimeField(auto_now_add=True)),
|
||||
('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)),
|
||||
('applicant_email', models.EmailField(blank=True, help_text='Email of the applicant', max_length=254)),
|
||||
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Submission',
|
||||
'verbose_name_plural': 'Form Submissions',
|
||||
'ordering': ['-submitted_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FieldResponse',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
|
||||
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
|
||||
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Field Response',
|
||||
'verbose_name_plural': 'Field Responses',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='Name of the form template', max_length=200)),
|
||||
('description', models.TextField(blank=True, help_text='Description of the form template')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this template is active')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Template',
|
||||
'verbose_name_plural': 'Form Templates',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formsubmission',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formstage',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Candidate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('resume', models.FileField(upload_to='resumes/')),
|
||||
('parsed_summary', models.TextField(blank=True)),
|
||||
('status', models.CharField(default='Applied', max_length=100)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
||||
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')),
|
||||
('exam_date', models.DateField(blank=True, null=True, verbose_name='Exam Date')),
|
||||
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')),
|
||||
('interview_date', models.DateField(blank=True, null=True, verbose_name='Interview Date')),
|
||||
('interview_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Interview Status')),
|
||||
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
|
||||
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')),
|
||||
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
|
||||
('match_score', models.IntegerField(blank=True, null=True)),
|
||||
('strengths', models.TextField(blank=True)),
|
||||
('weaknesses', models.TextField(blank=True)),
|
||||
('criteria_checklist', models.JSONField(blank=True, default=dict)),
|
||||
('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Candidate',
|
||||
'verbose_name_plural': 'Candidates',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobPosting',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('department', models.CharField(blank=True, max_length=100)),
|
||||
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
|
||||
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
|
||||
('location_city', models.CharField(blank=True, max_length=100)),
|
||||
('location_state', models.CharField(blank=True, max_length=100)),
|
||||
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
|
||||
('description', models.TextField(help_text='Full job description including responsibilities and requirements')),
|
||||
('qualifications', models.TextField(blank=True, help_text='Required qualifications and skills')),
|
||||
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
|
||||
('benefits', models.TextField(blank=True, help_text='Benefits offered')),
|
||||
('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])),
|
||||
('application_deadline', models.DateField(blank=True, null=True)),
|
||||
('application_instructions', models.TextField(blank=True, help_text='Special instructions for applicants')),
|
||||
('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
|
||||
('status', models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True)),
|
||||
('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])),
|
||||
('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)),
|
||||
('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')),
|
||||
('posted_to_linkedin', models.BooleanField(default=False)),
|
||||
('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)),
|
||||
('linkedin_posted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True)),
|
||||
('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)),
|
||||
('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)),
|
||||
('start_date', models.DateField(blank=True, help_text='Desired start date', null=True)),
|
||||
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')),
|
||||
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Job Posting',
|
||||
'verbose_name_plural': 'Job Postings',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterviewSchedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('start_date', models.DateField(verbose_name='Start Date')),
|
||||
('end_date', models.DateField(verbose_name='End Date')),
|
||||
('working_days', models.JSONField(verbose_name='Working Days')),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
|
||||
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.job')),
|
||||
('breaks', models.ManyToManyField(blank=True, related_name='schedules', to='recruitment.breaktime')),
|
||||
('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formtemplate',
|
||||
name='job',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='job',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedFormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
|
||||
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
|
||||
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Shared Form Template',
|
||||
'verbose_name_plural': 'Shared Form Templates',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IntegrationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')),
|
||||
('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')),
|
||||
('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')),
|
||||
('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')),
|
||||
('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')),
|
||||
('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')),
|
||||
('error_message', models.TextField(blank=True, verbose_name='Error Message')),
|
||||
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
|
||||
('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')),
|
||||
('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')),
|
||||
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Integration Log',
|
||||
'verbose_name_plural': 'Integration Logs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrainingMaterial',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('content', models.TextField(blank=True, verbose_name='Content')),
|
||||
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
|
||||
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Training Material',
|
||||
'verbose_name_plural': 'Training Materials',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScheduledInterview',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('interview_date', models.DateField(verbose_name='Interview Date')),
|
||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
|
||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
19
recruitment/migrations/0002_candidate_address.py
Normal file
19
recruitment/migrations/0002_candidate_address.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 17:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='address',
|
||||
field=models.TextField(default='', max_length=200, verbose_name='Address'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
19
recruitment/migrations/0003_scheduledinterview_slug.py
Normal file
19
recruitment/migrations/0003_scheduledinterview_slug.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 17:47
|
||||
|
||||
import django_extensions.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0002_candidate_address'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scheduledinterview',
|
||||
name='slug',
|
||||
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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 = [
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -24,15 +24,6 @@ import asyncio
|
||||
|
||||
@receiver(post_save, sender=models.Candidate)
|
||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
# Skip if no resume or OpenRouter not configured
|
||||
if instance.resume:
|
||||
return
|
||||
if kwargs.get('update_fields') is not None:
|
||||
return
|
||||
|
||||
# Optional: Only re-score if resume changed (advanced: track file hash)
|
||||
# For simplicity, we score on every save with a resume
|
||||
|
||||
try:
|
||||
# Get absolute file path
|
||||
file_path = instance.resume.path
|
||||
@ -144,7 +135,7 @@ def create_default_stages(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Create default resume stages when a new FormTemplate is created
|
||||
"""
|
||||
if created: # Only run for new templates, not updates
|
||||
if created:
|
||||
with transaction.atomic():
|
||||
# Stage 1: Contact Information
|
||||
contact_stage = FormStage.objects.create(
|
||||
@ -155,18 +146,26 @@ def create_default_stages(sender, instance, created, **kwargs):
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=contact_stage,
|
||||
label='Full Name',
|
||||
label='First Name',
|
||||
field_type='text',
|
||||
required=True,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=contact_stage,
|
||||
label='Last Name',
|
||||
field_type='text',
|
||||
required=True,
|
||||
order=1,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=contact_stage,
|
||||
label='Email Address',
|
||||
field_type='email',
|
||||
required=True,
|
||||
order=1,
|
||||
order=2,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
@ -174,7 +173,7 @@ def create_default_stages(sender, instance, created, **kwargs):
|
||||
label='Phone Number',
|
||||
field_type='phone',
|
||||
required=True,
|
||||
order=2,
|
||||
order=3,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
@ -182,7 +181,7 @@ def create_default_stages(sender, instance, created, **kwargs):
|
||||
label='Address',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=3,
|
||||
order=4,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
@ -190,10 +189,10 @@ def create_default_stages(sender, instance, created, **kwargs):
|
||||
label='Resume Upload',
|
||||
field_type='file',
|
||||
required=True,
|
||||
order=4,
|
||||
order=5,
|
||||
is_predefined=True,
|
||||
file_types='.pdf,.doc,.docx',
|
||||
max_file_size=5
|
||||
max_file_size=1
|
||||
)
|
||||
|
||||
# Stage 2: Resume Objective
|
||||
|
||||
@ -63,7 +63,8 @@ urlpatterns = [
|
||||
|
||||
path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
||||
path('forms/<int:form_id>/submissions/<int:submission_id>/', views.form_submission_details, name='form_submission_details'),
|
||||
path('forms/<int:form_id>/submissions/<int:s>/', views.form_submission_details, name='form_submission_details'),
|
||||
path('forms/template/<slug:slug>/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'),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import json
|
||||
import requests
|
||||
from rich import print
|
||||
from django.template.loader import render_to_string
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import JsonResponse
|
||||
@ -10,19 +11,23 @@ from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm
|
||||
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm,BreakTimeFormSet
|
||||
from rest_framework import viewsets
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator
|
||||
from .linkedin_service import LinkedInService
|
||||
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule
|
||||
from .models import ZoomMeeting, Candidate, JobPosting
|
||||
from .serializers import JobPostingSerializer, CandidateSerializer
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
||||
from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,schedule_interviews,get_available_time_slots
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule,BreakTime, ZoomMeeting, Candidate, JobPosting
|
||||
import logging
|
||||
from datastar_py.django import (
|
||||
DatastarResponse,
|
||||
ServerSentEventGenerator as SSE,
|
||||
read_signals,
|
||||
)
|
||||
|
||||
logger=logging.getLogger(__name__)
|
||||
|
||||
@ -194,7 +199,6 @@ def create_job(request):
|
||||
|
||||
|
||||
|
||||
|
||||
def edit_job(request,slug):
|
||||
"""Edit an existing job posting"""
|
||||
if request.method=='POST':
|
||||
@ -575,14 +579,8 @@ def applicant_job_detail(request,slug):
|
||||
# submissions = form.submissions.all().order_by('-submitted_at')
|
||||
|
||||
# # Pagination
|
||||
# paginator = Paginator(submissions, 20)
|
||||
# page_number = request.GET.get('page')
|
||||
# page_obj = paginator.get_page(page_number)
|
||||
#
|
||||
|
||||
# return render(request, 'forms/form_submissions.html', {
|
||||
# 'form': form,
|
||||
# 'page_obj': page_obj
|
||||
# })
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@ -732,9 +730,8 @@ def create_form_template(request):
|
||||
template = form.save(commit=False)
|
||||
template.created_by = request.user
|
||||
template.save()
|
||||
|
||||
messages.success(request, f'Form template "{template.name}" created successfully!')
|
||||
return redirect('form_builder', template_id=template.id)
|
||||
return redirect('form_templates_list')
|
||||
else:
|
||||
form = FormTemplateForm()
|
||||
|
||||
@ -770,15 +767,19 @@ def submit_form(request, template_id):
|
||||
"""Handle form submission"""
|
||||
try:
|
||||
template = get_object_or_404(FormTemplate, id=template_id)
|
||||
print(template)
|
||||
|
||||
# Create form submission
|
||||
submission = FormSubmission.objects.create(
|
||||
template=template,
|
||||
applicant_name=request.POST.get('applicant_name', ''),
|
||||
applicant_email=request.POST.get('applicant_email', '')
|
||||
)
|
||||
# # Create form submission
|
||||
# print({key: value for key, value in request.POST.items()})
|
||||
# first_name = next((value for key, value in request.POST.items() if key == 'First Name'), None)
|
||||
# last_name = next((value for key, value in request.POST.items() if key == 'Last Name'), None)
|
||||
# email = next((value for key, value in request.POST.items() if key == 'Email Address'), None)
|
||||
# phone = next((value for key, value in request.POST.items() if key == 'Phone Number'), None)
|
||||
# address = next((value for key, value in request.POST.items() if key == 'Address'), None)
|
||||
# resume = next((value for key, value in request.POST.items() if key == 'Resume Upload'), None)
|
||||
# print(first_name, last_name, email, phone, address, resume)
|
||||
# create candidate
|
||||
|
||||
submission = FormSubmission.objects.create(template=template)
|
||||
# Process field responses
|
||||
for field_id, value in request.POST.items():
|
||||
if field_id.startswith('field_'):
|
||||
@ -806,7 +807,29 @@ def submit_form(request, template_id):
|
||||
)
|
||||
except FormField.DoesNotExist:
|
||||
continue
|
||||
try:
|
||||
first_name = submission.responses.get(field__label="First Name")
|
||||
last_name = submission.responses.get(field__label="Last Name")
|
||||
email = submission.responses.get(field__label="Email Address")
|
||||
phone = submission.responses.get(field__label="Phone Number")
|
||||
address = submission.responses.get(field__label="Address")
|
||||
resume = submission.responses.get(field__label="Resume Upload")
|
||||
|
||||
submission.applicant_name = f"{first_name.display_value} {last_name.display_value}"
|
||||
submission.applicant_email = email.display_value
|
||||
submission.save()
|
||||
Candidate.objects.create(
|
||||
first_name=first_name.display_value,
|
||||
last_name=last_name.display_value,
|
||||
email=email.display_value,
|
||||
phone=phone.display_value,
|
||||
address=address.display_value,
|
||||
resume=resume.get_file if resume.is_file else None,
|
||||
job=submission.template.job
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Candidate creation failed,{e}")
|
||||
pass
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Form submitted successfully!',
|
||||
@ -818,16 +841,32 @@ def submit_form(request, template_id):
|
||||
'error': str(e)
|
||||
}, status=400)
|
||||
|
||||
def form_submission_details(request, form_id, submission_id):
|
||||
def form_template_submissions_list(request, template_slug):
|
||||
"""List all submissions for a specific form template"""
|
||||
template = get_object_or_404(FormTemplate, slug=template_slug, created_by=request.user)
|
||||
|
||||
submissions = FormSubmission.objects.filter(template=template).order_by('-submitted_at')
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(submissions, 10) # Show 10 submissions per page
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
return render(request, 'forms/form_template_submissions_list.html', {
|
||||
'template': template,
|
||||
'page_obj': page_obj
|
||||
})
|
||||
|
||||
def form_submission_details(request, template_id, submission_id):
|
||||
"""Display detailed view of a specific form submission"""
|
||||
# Get the form template and verify ownership
|
||||
form = get_object_or_404(FormTemplate, id=form_id, created_by=request.user)
|
||||
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
|
||||
|
||||
# Get the specific submission
|
||||
submission = get_object_or_404(FormSubmission, id=submission_id, template=form)
|
||||
submission = get_object_or_404(FormSubmission, id=submission_id, template=template)
|
||||
|
||||
# Get all stages with their fields
|
||||
stages = form.stages.prefetch_related('fields').order_by('order')
|
||||
stages = template.stages.prefetch_related('fields').order_by('order')
|
||||
|
||||
# Get all responses for this submission, ordered by field order
|
||||
responses = submission.responses.select_related('field').order_by('field__order')
|
||||
@ -839,22 +878,21 @@ def form_submission_details(request, form_id, submission_id):
|
||||
'stage': stage,
|
||||
'responses': responses.filter(field__stage=stage)
|
||||
}
|
||||
# print(stages)
|
||||
|
||||
return render(request, 'forms/form_submission_details.html', {
|
||||
'form': form,
|
||||
'template': template,
|
||||
'submission': submission,
|
||||
'stages': stages,
|
||||
'responses': responses,
|
||||
'stage_responses': stage_responses
|
||||
})
|
||||
|
||||
|
||||
|
||||
def schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
def schedule_interviews_view(request, job_id):
|
||||
job = get_object_or_404(Job, id=job_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = InterviewScheduleForm(slug, request.POST)
|
||||
form = InterviewScheduleForm(job_id, request.POST)
|
||||
break_formset = BreakTimeFormSet(request.POST)
|
||||
|
||||
# Check if this is a confirmation request
|
||||
if 'confirm_schedule' in request.POST:
|
||||
@ -862,7 +900,7 @@ def schedule_interviews_view(request, slug):
|
||||
schedule_data = request.session.get('interview_schedule_data')
|
||||
if not schedule_data:
|
||||
messages.error(request, "Session expired. Please try again.")
|
||||
return redirect('schedule_interviews', slug=slug)
|
||||
return redirect('schedule_interviews', job_id=job_id)
|
||||
|
||||
# Create the interview schedule
|
||||
schedule = InterviewSchedule.objects.create(
|
||||
@ -875,6 +913,15 @@ def schedule_interviews_view(request, slug):
|
||||
candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids'])
|
||||
schedule.candidates.set(candidates)
|
||||
|
||||
# Add break times to the schedule
|
||||
if 'breaks' in schedule_data and schedule_data['breaks']:
|
||||
for break_data in schedule_data['breaks']:
|
||||
break_time = BreakTime.objects.create(
|
||||
start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(),
|
||||
end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time()
|
||||
)
|
||||
schedule.breaks.add(break_time)
|
||||
|
||||
# Schedule the interviews
|
||||
try:
|
||||
scheduled_count = schedule_interviews(schedule)
|
||||
@ -885,16 +932,16 @@ def schedule_interviews_view(request, slug):
|
||||
# Clear the session data
|
||||
if 'interview_schedule_data' in request.session:
|
||||
del request.session['interview_schedule_data']
|
||||
return redirect('job_detail', slug=slug)
|
||||
return redirect('job_detail', pk=job_id)
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
f"Error scheduling interviews: {str(e)}"
|
||||
)
|
||||
return redirect('schedule_interviews', slug=slug)
|
||||
return redirect('schedule_interviews', job_id=job_id)
|
||||
|
||||
# This is the initial form submission
|
||||
if form.is_valid():
|
||||
if form.is_valid() and break_formset.is_valid():
|
||||
# Get the form data
|
||||
candidates = form.cleaned_data['candidates']
|
||||
start_date = form.cleaned_data['start_date']
|
||||
@ -902,11 +949,18 @@ def schedule_interviews_view(request, slug):
|
||||
working_days = form.cleaned_data['working_days']
|
||||
start_time = form.cleaned_data['start_time']
|
||||
end_time = form.cleaned_data['end_time']
|
||||
break_start_time = form.cleaned_data['break_start_time']
|
||||
break_end_time = form.cleaned_data['break_end_time']
|
||||
interview_duration = form.cleaned_data['interview_duration']
|
||||
buffer_time = form.cleaned_data['buffer_time']
|
||||
|
||||
# Process break times
|
||||
breaks = []
|
||||
for break_form in break_formset:
|
||||
if break_form.cleaned_data and not break_form.cleaned_data.get('DELETE'):
|
||||
breaks.append({
|
||||
'start_time': break_form.cleaned_data['start_time'].isoformat(),
|
||||
'end_time': break_form.cleaned_data['end_time'].isoformat()
|
||||
})
|
||||
|
||||
# Create a temporary schedule object (not saved to DB)
|
||||
temp_schedule = InterviewSchedule(
|
||||
job=job,
|
||||
@ -915,14 +969,20 @@ def schedule_interviews_view(request, slug):
|
||||
working_days=working_days,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
break_start_time=break_start_time,
|
||||
break_end_time=break_end_time,
|
||||
interview_duration=interview_duration,
|
||||
buffer_time=buffer_time
|
||||
)
|
||||
|
||||
# Create temporary break time objects
|
||||
temp_breaks = []
|
||||
for break_data in breaks:
|
||||
temp_breaks.append(BreakTime(
|
||||
start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(),
|
||||
end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time()
|
||||
))
|
||||
|
||||
# Get available slots
|
||||
available_slots = get_available_time_slots(temp_schedule)
|
||||
available_slots = get_available_time_slots(temp_schedule, temp_breaks)
|
||||
|
||||
if len(available_slots) < len(candidates):
|
||||
messages.error(
|
||||
@ -931,6 +991,7 @@ def schedule_interviews_view(request, slug):
|
||||
)
|
||||
return render(request, 'interviews/schedule_interviews.html', {
|
||||
'form': form,
|
||||
'break_formset': break_formset,
|
||||
'job': job
|
||||
})
|
||||
|
||||
@ -951,11 +1012,10 @@ def schedule_interviews_view(request, slug):
|
||||
'working_days': working_days,
|
||||
'start_time': start_time.isoformat(),
|
||||
'end_time': end_time.isoformat(),
|
||||
'break_start_time': break_start_time.isoformat() if break_start_time else None,
|
||||
'break_end_time': break_end_time.isoformat() if break_end_time else None,
|
||||
'interview_duration': interview_duration,
|
||||
'buffer_time': buffer_time,
|
||||
'candidate_ids': [c.id for c in candidates]
|
||||
'candidate_ids': [c.id for c in candidates],
|
||||
'breaks': breaks
|
||||
}
|
||||
request.session['interview_schedule_data'] = schedule_data
|
||||
|
||||
@ -968,15 +1028,16 @@ def schedule_interviews_view(request, slug):
|
||||
'working_days': working_days,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'break_start_time': break_start_time,
|
||||
'break_end_time': break_end_time,
|
||||
'breaks': breaks,
|
||||
'interview_duration': interview_duration,
|
||||
'buffer_time': buffer_time
|
||||
})
|
||||
else:
|
||||
form = InterviewScheduleForm(slug=slug)
|
||||
form = InterviewScheduleForm(job_id=job_id)
|
||||
break_formset = BreakTimeFormSet()
|
||||
|
||||
return render(request, 'interviews/schedule_interviews.html', {
|
||||
'form': form,
|
||||
'break_formset': break_formset,
|
||||
'job': job
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{% load i18n static %}
|
||||
{% load partials %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
||||
<head>
|
||||
@ -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 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<main class="container flex-grow-1">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
<footer class="footer mt-auto">
|
||||
<div class="container text-center">
|
||||
<p class="mb-0">
|
||||
|
||||
103
templates/forms/form_template_submissions_list.html
Normal file
103
templates/forms/form_template_submissions_list.html
Normal file
@ -0,0 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Submissions for {{ template.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<nav class="mb-6">
|
||||
<a href="{% url 'form_templates_list' %}" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back to Form Templates
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Submissions for: <span class="text-blue-600 dark:text-blue-400">{{ template.name }}</span></h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Template ID: {{ template.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
{% if page_obj.object_list %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submission ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Email</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submitted At</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for submission in page_obj %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-750">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ submission.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_name|default:"N/A" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_email|default:"N/A" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.submitted_at|date:"Y-m-d H:i:s" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center">
|
||||
View Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav class="mt-6 flex items-center justify-between" aria-label="Pagination">
|
||||
<div class="hidden sm:block">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Showing <span class="font-medium">{{ page_obj.start_index }}</span> to <span class="font-medium">{{ page_obj.end_index }}</span> of <span class="font-medium">{{ page_obj.paginator.count }}</span> results.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-1 flex justify-between sm:justify-end mt-4 sm:mt-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-lg font-medium text-gray-900 dark:text-white">No submissions found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">There are no submissions for this form template yet.</p>
|
||||
<div class="mt-6">
|
||||
<a href="{% url 'form_templates_list' %}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-2 -ml-1 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
Back to Templates
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
{% load partials %}
|
||||
|
||||
{% block title %}Form Templates - ATS{% endblock %}
|
||||
|
||||
@ -229,7 +230,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if templates %}
|
||||
<div class="row g-4">
|
||||
{% for template in templates %}
|
||||
@ -276,6 +276,9 @@
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Submissions" %}
|
||||
</a>
|
||||
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
||||
data-template-id="{{ template.id }}"
|
||||
data-template-name="{{ template.name }}">
|
||||
@ -337,6 +340,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/delete_modal.html' %}
|
||||
@ -352,6 +356,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% url 'create_form_template' as create_form_template_url %}
|
||||
<form id="createTemplateForm" method="post" action="{% url 'create_form_template' %}">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
@ -366,186 +371,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// JS logic remains the same as previous versions but ensures Django variables are handled
|
||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
|
||||
let toastContainer = document.querySelector('.toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
function createToast(message, type = 'success') {
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const iconClass = type === 'success' ? 'check-circle text-success' : 'exclamation-circle text-danger';
|
||||
const title = type === 'success' ? 'Success' : 'Error';
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000">
|
||||
<div class="toast-header">
|
||||
<i class="fas fa-${iconClass} me-2"></i>
|
||||
<strong class="me-auto">${title}</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement);
|
||||
toast.show();
|
||||
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality - handles submission on Enter key
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const query = this.value;
|
||||
// Assumes 'form_templates_list' is the view name for this page
|
||||
window.location.href = query ? `?q=${encodeURIComponent(query)}` : '{% url "form_templates_list" %}';
|
||||
}
|
||||
});
|
||||
|
||||
// Bind search form submit to the main button click event for consistency
|
||||
document.querySelector('.filter-buttons button[type="submit"]').addEventListener('click', function(e) {
|
||||
// Prevent default submission to handle URL construction correctly
|
||||
e.preventDefault();
|
||||
const query = document.getElementById('searchInput').value;
|
||||
window.location.href = query ? `?q=${encodeURIComponent(query)}` : '{% url "form_templates_list" %}';
|
||||
});
|
||||
|
||||
|
||||
// Delete modal functionality
|
||||
let templateToDelete = null;
|
||||
|
||||
document.querySelectorAll('.delete').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const templateId = this.dataset.templateId;
|
||||
const templateName = this.dataset.templateName;
|
||||
templateToDelete = templateId;
|
||||
document.getElementById('templateNameToDelete').textContent = templateName;
|
||||
deleteModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle form submission in delete modal
|
||||
document.getElementById('deleteForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!templateToDelete) return;
|
||||
|
||||
// This relies on 'csrfToken' being defined somewhere, which is typical for Django templates.
|
||||
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
try {
|
||||
// NOTE: Update this URL to match your actual Django API endpoint for deletion
|
||||
const response = await fetch(`/api/templates/${templateToDelete}/delete/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
createToast(result.message);
|
||||
deleteModal.hide();
|
||||
|
||||
// Smoothly remove the card
|
||||
setTimeout(() => {
|
||||
const buttonClicked = document.querySelector(`button[data-template-id="${templateToDelete}"]`);
|
||||
if(buttonClicked) {
|
||||
const cardToRemove = buttonClicked.closest('.col-lg-4');
|
||||
if (cardToRemove) {
|
||||
cardToRemove.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
cardToRemove.style.opacity = '0';
|
||||
cardToRemove.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
cardToRemove.remove();
|
||||
|
||||
// Reload if the last card is removed to show the empty state
|
||||
const remainingCards = document.querySelectorAll('.col-lg-4');
|
||||
if (remainingCards.length === 0) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
createToast('Error: ' + (result.error || 'Could not delete template.'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
createToast('An error occurred while deleting the template.', 'error');
|
||||
}
|
||||
|
||||
templateToDelete = null;
|
||||
});
|
||||
|
||||
// Handle modal close event
|
||||
document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() {
|
||||
templateToDelete = null;
|
||||
});
|
||||
|
||||
// Handle create template form submission
|
||||
document.getElementById('createTemplateForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
// Show success toast
|
||||
createToast(result.message || 'Template created successfully!');
|
||||
|
||||
// Close modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('createTemplateModal')).hide();
|
||||
|
||||
// Clear form
|
||||
form.reset();
|
||||
|
||||
// Redirect to form builder with new template ID
|
||||
if (result.template_id) {
|
||||
window.location.href = `{% url 'form_builder' %}${result.template_id}/`;
|
||||
} else {
|
||||
// Fallback to template list if no ID is returned
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
// Show error toast
|
||||
createToast('Error: ' + (result.message || 'Could not create template.'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
createToast('An error occurred while creating the template.', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -7,19 +7,41 @@
|
||||
<title>{% translate "Application Form" %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
/* KAAT-S Theme Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e; /* Main Primary Color */
|
||||
--kaauh-teal-dark: #004a53; /* Dark Primary Color */
|
||||
|
||||
/* Mapping wizard defaults to theme colors */
|
||||
--primary: var(--kaauh-teal);
|
||||
--primary-light: #007c89; /* Slightly lighter shade for subtle hover/border */
|
||||
--secondary: var(--kaauh-teal-dark);
|
||||
--success: #198754; /* Keeping a standard success green for Submit */
|
||||
--error: #dc3545; /* Standard danger red */
|
||||
|
||||
--light: #f8f9fa;
|
||||
--dark: #212529;
|
||||
--gray: #6c757d;
|
||||
--light-gray: #e9ecef;
|
||||
--border: #dee2e6;
|
||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--radius: 16px; /* Increased radius for a softer look */
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
<style>
|
||||
/* KAAT-S Theme Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e; /* Main Primary Color */
|
||||
--kaauh-teal-dark: #004a53; /* Dark Primary Color */
|
||||
|
||||
|
||||
/* Mapping wizard defaults to theme colors */
|
||||
--primary: var(--kaauh-teal);
|
||||
--primary-light: #007c89; /* Slightly lighter shade for subtle hover/border */
|
||||
--secondary: var(--kaauh-teal-dark);
|
||||
--success: #198754; /* Keeping a standard success green for Submit */
|
||||
--error: #dc3545; /* Standard danger red */
|
||||
|
||||
|
||||
--light: #f8f9fa;
|
||||
--dark: #212529;
|
||||
--gray: #6c757d;
|
||||
@ -33,7 +55,7 @@
|
||||
body {
|
||||
/* Remove centering/flex properties to allow for normal document flow and scrolling */
|
||||
padding-top: 56px; /* Space for the sticky navbar */
|
||||
|
||||
|
||||
/* Dark gradient background to match the theme */
|
||||
background: linear-gradient(135deg, var(--kaauh-teal-dark) 0%, #1e3a47 100%);
|
||||
background-image: url("{% static 'image/vision.svg' %}");
|
||||
@ -44,7 +66,7 @@
|
||||
padding: 0; /* Remove padding from body */
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* Wrapper to center the wizard content below the navbar */
|
||||
.page-content-wrapper {
|
||||
display: flex;
|
||||
@ -64,8 +86,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Allow height to be determined by content, constrained by max-height */
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
@ -299,13 +321,23 @@
|
||||
background: rgba(0, 99, 110, 0.05); /* Light teal background */
|
||||
}
|
||||
|
||||
.option-item.error {
|
||||
border-color: var(--error);
|
||||
background: rgba(220, 53, 69, 0.05);
|
||||
}
|
||||
|
||||
/* Ensures radio/checkbox controls themselves use the primary color */
|
||||
.option-item input[type="radio"]:checked,
|
||||
.option-item input[type="checkbox"]:checked {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
.option-item.error {
|
||||
border-color: var(--error);
|
||||
background: rgba(220, 53, 69, 0.05);
|
||||
}
|
||||
|
||||
|
||||
/* Ensures radio/checkbox controls themselves use the primary color */
|
||||
.option-item input[type="radio"]:checked,
|
||||
.option-item input[type="radio"]:checked,
|
||||
.option-item input[type="checkbox"]:checked {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
@ -423,7 +455,7 @@
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* === FIX FOR SMALL-SCREEN HAMBURGER MENU === */
|
||||
@media (max-width: 991.98px) {
|
||||
/* Add vertical spacing to the navigation items when the navbar is collapsed */
|
||||
@ -431,15 +463,15 @@
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
#navbarNav .nav-item:last-child {
|
||||
border-bottom: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
#navbarNav .nav-link {
|
||||
padding: 8px 15px;
|
||||
padding: 8px 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -451,13 +483,13 @@
|
||||
|
||||
#bottomNavbar {
|
||||
/* Position the dark navbar 72px from the top of the viewport */
|
||||
top: 72px;
|
||||
top: 72px;
|
||||
/* The z-index is already 1030 in the inline style, which is correct */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1030;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand text-white fw-bold" href="/">
|
||||
@ -469,7 +501,7 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="/applications/">{% translate "Applications" %}</a>
|
||||
</li>
|
||||
@ -486,7 +518,7 @@
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
|
||||
<span class="ms-2 text-white">JOB ID: {{job_id}}</span>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="page-content-wrapper">
|
||||
<div class="wizard-container">
|
||||
<div class="progress-container">
|
||||
@ -529,10 +561,10 @@
|
||||
// Placeholder for the complete JavaScript logic (omitted for brevity, but required for functionality)
|
||||
// This script block should contain the Application State, DOM Elements, Validation, API, Rendering, and Event Handlers
|
||||
console.log("JavaScript logic for form wizard needs to be included here.");
|
||||
|
||||
|
||||
// --- COMPLETE JAVASCRIPT LOGIC GOES HERE ---
|
||||
// (The logic provided in the previous turn, including the completed createFieldElement and renderPreview functions)
|
||||
|
||||
|
||||
// Example structure for reference:
|
||||
// function validateEmail(email) { ... }
|
||||
// function loadFormTemplate() { ... }
|
||||
@ -801,8 +833,8 @@
|
||||
const formData = new FormData();
|
||||
|
||||
// Add applicant info
|
||||
formData.append('applicant_name', state.formData.applicant_name || '');
|
||||
formData.append('applicant_email', state.formData.applicant_email || '');
|
||||
//formData.append('applicant_name', state.formData.applicant_name || '');
|
||||
//formData.append('applicant_email', state.formData.applicant_email || '');
|
||||
|
||||
// Add field responses
|
||||
state.stages.forEach(stage => {
|
||||
|
||||
@ -27,8 +27,13 @@
|
||||
<p><strong>Working Hours:</strong> {{ start_time|time:"g:i A" }} to {{ end_time|time:"g:i A" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if break_start_time and break_end_time %}
|
||||
<p><strong>Break Time:</strong> {{ break_start_time|time:"g:i A" }} to {{ break_end_time|time:"g:i A" }}</p>
|
||||
{% if breaks %}
|
||||
<p><strong>Break Times:</strong></p>
|
||||
<ul>
|
||||
{% for break in breaks %}
|
||||
<li>{{ break.start_time|time:"g:i A" }} to {{ break.end_time|time:"g:i A" }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p><strong>Interview Duration:</strong> {{ interview_duration }} minutes</p>
|
||||
<p><strong>Buffer Time:</strong> {{ buffer_time }} minutes</p>
|
||||
@ -75,7 +80,7 @@
|
||||
<button type="submit" name="confirm_schedule" class="btn btn-success">
|
||||
<i class="fas fa-check"></i> Confirm Schedule
|
||||
</button>
|
||||
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary">
|
||||
<a href="{% url 'schedule_interviews' job_id=job.id %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Edit
|
||||
</a>
|
||||
</form>
|
||||
@ -109,13 +114,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
},
|
||||
{% endfor %}
|
||||
{% for break in breaks %}
|
||||
{
|
||||
title: 'Break',
|
||||
start: '{{ start_date|date:"Y-m-d" }}T{{ break.start_time|time:"H:i:s" }}',
|
||||
end: '{{ start_date|date:"Y-m-d" }}T{{ break.end_time|time:"H:i:s" }}',
|
||||
color: '#ff9f89',
|
||||
display: 'background'
|
||||
},
|
||||
{% endfor %}
|
||||
],
|
||||
eventClick: function(info) {
|
||||
// Show candidate details in a modal or alert
|
||||
alert('Candidate: ' + info.event.title +
|
||||
'\nDate: ' + info.event.start.toLocaleDateString() +
|
||||
'\nTime: ' + info.event.extendedProps.time +
|
||||
'\nEmail: ' + info.event.extendedProps.email);
|
||||
if (info.event.title !== 'Break') {
|
||||
alert('Candidate: ' + info.event.title +
|
||||
'\nDate: ' + info.event.start.toLocaleDateString() +
|
||||
'\nTime: ' + info.event.extendedProps.time +
|
||||
'\nEmail: ' + info.event.extendedProps.email);
|
||||
}
|
||||
info.jsEvent.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<form method="post" id="schedule-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
@ -21,31 +21,31 @@
|
||||
<div class="col-md-6">
|
||||
<h5>Schedule Details</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.start_date.id_for_label }}">Start Date</label>
|
||||
{{ form.start_date }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.end_date.id_for_label }}">End Date</label>
|
||||
{{ form.end_date }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label>Working Days</label>
|
||||
{{ form.working_days }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.start_time.id_for_label }}">Start Time</label>
|
||||
{{ form.start_time }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.end_time.id_for_label }}">End Time</label>
|
||||
{{ form.end_time }}
|
||||
</div>
|
||||
@ -54,30 +54,14 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.break_start_time.id_for_label }}">Break Start Time</label>
|
||||
{{ form.break_start_time }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.break_end_time.id_for_label }}">Break End Time</label>
|
||||
{{ form.break_end_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.interview_duration.id_for_label }}">Interview Duration (minutes)</label>
|
||||
{{ form.interview_duration }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.buffer_time.id_for_label }}">Buffer Time (minutes)</label>
|
||||
{{ form.buffer_time }}
|
||||
</div>
|
||||
@ -86,12 +70,85 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h5>Break Times</h5>
|
||||
<div id="break-times-container">
|
||||
{{ break_formset.management_form }}
|
||||
{% for form in break_formset %}
|
||||
<div class="break-time-form row mb-2">
|
||||
<div class="col-md-5">
|
||||
<label>Start Time</label>
|
||||
{{ form.start_time }}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label>End Time</label>
|
||||
{{ form.end_time }}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label> </label><br>
|
||||
{{ form.DELETE }}
|
||||
<button type="button" class="btn btn-danger btn-sm remove-break">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" id="add-break" class="btn btn-secondary btn-sm mt-2">Add Break</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">Schedule Interviews</button>
|
||||
<a href="{% url 'job_detail' slug=job.slug %}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Preview Schedule</button>
|
||||
<a href="{% url 'job_detail' pk=job.id %}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const addBreakBtn = document.getElementById('add-break');
|
||||
const breakTimesContainer = document.getElementById('break-times-container');
|
||||
const totalFormsInput = document.getElementById('id_breaks-TOTAL_FORMS');
|
||||
|
||||
addBreakBtn.addEventListener('click', function() {
|
||||
const formCount = parseInt(totalFormsInput.value);
|
||||
const newFormHtml = `
|
||||
<div class="break-time-form row mb-2">
|
||||
<div class="col-md-5">
|
||||
<label>Start Time</label>
|
||||
<input type="time" name="breaks-${formCount}-start_time" class="form-control" id="id_breaks-${formCount}-start_time">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label>End Time</label>
|
||||
<input type="time" name="breaks-${formCount}-end_time" class="form-control" id="id_breaks-${formCount}-end_time">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label> </label><br>
|
||||
<input type="checkbox" name="breaks-${formCount}-DELETE" id="id_breaks-${formCount}-DELETE" style="display:none;">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-break">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = newFormHtml;
|
||||
const newForm = tempDiv.firstChild;
|
||||
|
||||
breakTimesContainer.appendChild(newForm);
|
||||
totalFormsInput.value = formCount + 1;
|
||||
});
|
||||
|
||||
// Handle remove button clicks
|
||||
breakTimesContainer.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-break')) {
|
||||
const form = e.target.closest('.break-time-form');
|
||||
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
|
||||
deleteCheckbox.checked = true;
|
||||
form.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -26,7 +26,7 @@
|
||||
transform: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
padding: 1.25rem;
|
||||
@ -52,7 +52,7 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Secondary Action Buttons (used in table) */
|
||||
.btn-group .btn-outline-primary {
|
||||
color: var(--kaauh-teal);
|
||||
@ -64,7 +64,7 @@
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
|
||||
/* Table Styling */
|
||||
.table thead th {
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -78,19 +78,19 @@
|
||||
.table tbody tr:hover {
|
||||
background-color: #f3f9f9; /* Light teal hover for rows */
|
||||
}
|
||||
|
||||
|
||||
.table tbody td {
|
||||
vertical-align: middle;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
|
||||
/* Badge Styling */
|
||||
.badge {
|
||||
font-weight: 600;
|
||||
padding: 0.4em 0.7em;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
|
||||
/* Status Badge Mapping */
|
||||
.table .bg-primary { background-color: var(--kaauh-teal) !important; color: white !important;} /* Job Title Badge */
|
||||
.table .bg-success { background-color: #28a745 !important; } /* Applied: Yes */
|
||||
@ -151,7 +151,7 @@
|
||||
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Phone" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Job" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Applied" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Stage" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Created" %}</th>
|
||||
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
@ -164,8 +164,8 @@
|
||||
<td>{{ candidate.phone }}</td>
|
||||
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
||||
<td>
|
||||
<span class="badge {% if candidate.applied %}bg-success{% else %}bg-warning{% endif %}">
|
||||
{{ candidate.applied|yesno:"Yes,No" }}
|
||||
<span class="badge bg-primary">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user