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 django import forms
|
||||||
from .validators import validate_hash_tags
|
from .validators import validate_hash_tags
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
|
from django.forms.formsets import formset_factory
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
|
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
|
from django_summernote.widgets import SummernoteWidget
|
||||||
|
|
||||||
|
|
||||||
class CandidateForm(forms.ModelForm):
|
class CandidateForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Candidate
|
model = Candidate
|
||||||
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume', 'stage']
|
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume',]
|
||||||
labels = {
|
labels = {
|
||||||
'first_name': _('First Name'),
|
'first_name': _('First Name'),
|
||||||
'last_name': _('Last Name'),
|
'last_name': _('Last Name'),
|
||||||
'phone': _('Phone'),
|
'phone': _('Phone'),
|
||||||
'email': _('Email'),
|
'email': _('Email'),
|
||||||
'resume': _('Resume'),
|
'resume': _('Resume'),
|
||||||
'stage': _('Application Stage'),
|
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter first name')}),
|
'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter first name')}),
|
||||||
@ -412,11 +412,21 @@ class FormTemplateForm(forms.ModelForm):
|
|||||||
Field('is_active', css_class='form-check-input'),
|
Field('is_active', css_class='form-check-input'),
|
||||||
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
|
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):
|
class InterviewScheduleForm(forms.ModelForm):
|
||||||
candidates = forms.ModelMultipleChoiceField(
|
candidates = forms.ModelMultipleChoiceField(
|
||||||
queryset=Candidate.objects.none(),
|
queryset=Candidate.objects.none(),
|
||||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
working_days = forms.MultipleChoiceField(
|
working_days = forms.MultipleChoiceField(
|
||||||
@ -429,7 +439,7 @@ class InterviewScheduleForm(forms.ModelForm):
|
|||||||
(5, 'Saturday'),
|
(5, 'Saturday'),
|
||||||
(6, 'Sunday'),
|
(6, 'Sunday'),
|
||||||
],
|
],
|
||||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -437,16 +447,15 @@ class InterviewScheduleForm(forms.ModelForm):
|
|||||||
model = InterviewSchedule
|
model = InterviewSchedule
|
||||||
fields = [
|
fields = [
|
||||||
'candidates', 'start_date', 'end_date', 'working_days',
|
'candidates', 'start_date', 'end_date', 'working_days',
|
||||||
'start_time', 'end_time', 'break_start_time', 'break_end_time',
|
'start_time', 'end_time', 'interview_duration', 'buffer_time'
|
||||||
'interview_duration', 'buffer_time'
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||||
'end_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'}),
|
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||||
'end_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'}),
|
'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, slug, *args, **kwargs):
|
def __init__(self, slug, *args, **kwargs):
|
||||||
@ -456,11 +465,7 @@ class InterviewScheduleForm(forms.ModelForm):
|
|||||||
job__slug=slug,
|
job__slug=slug,
|
||||||
stage='Interview'
|
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):
|
def clean_working_days(self):
|
||||||
working_days = self.cleaned_data.get('working_days')
|
working_days = self.cleaned_data.get('working_days')
|
||||||
# Convert string values to integers
|
# 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.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
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -9,32 +14,375 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Job',
|
name='BreakTime',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('title', models.CharField(max_length=255)),
|
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||||
('description_en', models.TextField()),
|
('end_time', models.TimeField(verbose_name='End Time')),
|
||||||
('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)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
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(
|
migrations.CreateModel(
|
||||||
name='Candidate',
|
name='Candidate',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=255)),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||||
('email', models.EmailField(max_length=254)),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||||
('resume', models.FileField(upload_to='resumes/')),
|
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||||
('parsed_summary', models.TextField(blank=True)),
|
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||||
('status', models.CharField(default='Applied', max_length=100)),
|
('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)),
|
('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)
|
@receiver(post_save, sender=models.Candidate)
|
||||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
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:
|
try:
|
||||||
# Get absolute file path
|
# Get absolute file path
|
||||||
file_path = instance.resume.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
|
Create default resume stages when a new FormTemplate is created
|
||||||
"""
|
"""
|
||||||
if created: # Only run for new templates, not updates
|
if created:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Stage 1: Contact Information
|
# Stage 1: Contact Information
|
||||||
contact_stage = FormStage.objects.create(
|
contact_stage = FormStage.objects.create(
|
||||||
@ -155,18 +146,26 @@ def create_default_stages(sender, instance, created, **kwargs):
|
|||||||
)
|
)
|
||||||
FormField.objects.create(
|
FormField.objects.create(
|
||||||
stage=contact_stage,
|
stage=contact_stage,
|
||||||
label='Full Name',
|
label='First Name',
|
||||||
field_type='text',
|
field_type='text',
|
||||||
required=True,
|
required=True,
|
||||||
order=0,
|
order=0,
|
||||||
is_predefined=True
|
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(
|
FormField.objects.create(
|
||||||
stage=contact_stage,
|
stage=contact_stage,
|
||||||
label='Email Address',
|
label='Email Address',
|
||||||
field_type='email',
|
field_type='email',
|
||||||
required=True,
|
required=True,
|
||||||
order=1,
|
order=2,
|
||||||
is_predefined=True
|
is_predefined=True
|
||||||
)
|
)
|
||||||
FormField.objects.create(
|
FormField.objects.create(
|
||||||
@ -174,7 +173,7 @@ def create_default_stages(sender, instance, created, **kwargs):
|
|||||||
label='Phone Number',
|
label='Phone Number',
|
||||||
field_type='phone',
|
field_type='phone',
|
||||||
required=True,
|
required=True,
|
||||||
order=2,
|
order=3,
|
||||||
is_predefined=True
|
is_predefined=True
|
||||||
)
|
)
|
||||||
FormField.objects.create(
|
FormField.objects.create(
|
||||||
@ -182,7 +181,7 @@ def create_default_stages(sender, instance, created, **kwargs):
|
|||||||
label='Address',
|
label='Address',
|
||||||
field_type='text',
|
field_type='text',
|
||||||
required=False,
|
required=False,
|
||||||
order=3,
|
order=4,
|
||||||
is_predefined=True
|
is_predefined=True
|
||||||
)
|
)
|
||||||
FormField.objects.create(
|
FormField.objects.create(
|
||||||
@ -190,10 +189,10 @@ def create_default_stages(sender, instance, created, **kwargs):
|
|||||||
label='Resume Upload',
|
label='Resume Upload',
|
||||||
field_type='file',
|
field_type='file',
|
||||||
required=True,
|
required=True,
|
||||||
order=4,
|
order=5,
|
||||||
is_predefined=True,
|
is_predefined=True,
|
||||||
file_types='.pdf,.doc,.docx',
|
file_types='.pdf,.doc,.docx',
|
||||||
max_file_size=5
|
max_file_size=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stage 2: Resume Objective
|
# 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>/', views.form_wizard_view, name='form_wizard'),
|
||||||
path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
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/', views.list_form_templates, name='list_form_templates'),
|
||||||
path('api/templates/save/', views.save_form_template, name='save_form_template'),
|
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,
|
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.
|
Generate a list of available time slots based on the schedule criteria.
|
||||||
Returns a list of dictionaries with 'date' and 'time' keys.
|
Returns a list of dictionaries with 'date' and 'time' keys.
|
||||||
@ -481,8 +481,6 @@ def get_available_time_slots(schedule):
|
|||||||
# Parse times
|
# Parse times
|
||||||
start_time = schedule.start_time
|
start_time = schedule.start_time
|
||||||
end_time = schedule.end_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)
|
# Calculate slot duration (interview duration + buffer time)
|
||||||
slot_duration = timedelta(minutes=schedule.interview_duration + schedule.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"Date range: {current_date} to {end_date}")
|
||||||
print(f"Time range: {start_time} to {end_time}")
|
print(f"Time range: {start_time} to {end_time}")
|
||||||
print(f"Slot duration: {slot_duration}")
|
print(f"Slot duration: {slot_duration}")
|
||||||
|
print(f"Breaks: {breaks}")
|
||||||
|
|
||||||
while current_date <= end_date:
|
while current_date <= end_date:
|
||||||
# Check if current day is a working day
|
# Check if current day is a working day
|
||||||
@ -510,13 +509,15 @@ def get_available_time_slots(schedule):
|
|||||||
if slot_end_time > end_time:
|
if slot_end_time > end_time:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if slot conflicts with break time
|
# Check if slot conflicts with any break time
|
||||||
conflict_with_break = False
|
conflict_with_break = False
|
||||||
if break_start and break_end:
|
if breaks:
|
||||||
# Check if the slot overlaps with break time
|
for break_time in breaks:
|
||||||
if not (current_time >= break_end or slot_end_time <= break_start):
|
# 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
|
conflict_with_break = True
|
||||||
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_start}-{break_end}")
|
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:
|
if not conflict_with_break:
|
||||||
# Add this slot to available slots
|
# Add this slot to available slots
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from rich import print
|
from rich import print
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
@ -10,19 +11,23 @@ from django.db.models import Q
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
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 rest_framework import viewsets
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from .linkedin_service import LinkedInService
|
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 .serializers import JobPostingSerializer, CandidateSerializer
|
||||||
from django.shortcuts import get_object_or_404, render, redirect
|
from django.shortcuts import get_object_or_404, render, redirect
|
||||||
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
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 .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 django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule,BreakTime, ZoomMeeting, Candidate, JobPosting
|
||||||
import logging
|
import logging
|
||||||
|
from datastar_py.django import (
|
||||||
|
DatastarResponse,
|
||||||
|
ServerSentEventGenerator as SSE,
|
||||||
|
read_signals,
|
||||||
|
)
|
||||||
|
|
||||||
logger=logging.getLogger(__name__)
|
logger=logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -194,7 +199,6 @@ def create_job(request):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def edit_job(request,slug):
|
def edit_job(request,slug):
|
||||||
"""Edit an existing job posting"""
|
"""Edit an existing job posting"""
|
||||||
if request.method=='POST':
|
if request.method=='POST':
|
||||||
@ -575,14 +579,8 @@ def applicant_job_detail(request,slug):
|
|||||||
# submissions = form.submissions.all().order_by('-submitted_at')
|
# submissions = form.submissions.all().order_by('-submitted_at')
|
||||||
|
|
||||||
# # Pagination
|
# # 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
|
@ensure_csrf_cookie
|
||||||
@ -732,9 +730,8 @@ def create_form_template(request):
|
|||||||
template = form.save(commit=False)
|
template = form.save(commit=False)
|
||||||
template.created_by = request.user
|
template.created_by = request.user
|
||||||
template.save()
|
template.save()
|
||||||
|
|
||||||
messages.success(request, f'Form template "{template.name}" created successfully!')
|
messages.success(request, f'Form template "{template.name}" created successfully!')
|
||||||
return redirect('form_builder', template_id=template.id)
|
return redirect('form_templates_list')
|
||||||
else:
|
else:
|
||||||
form = FormTemplateForm()
|
form = FormTemplateForm()
|
||||||
|
|
||||||
@ -770,15 +767,19 @@ def submit_form(request, template_id):
|
|||||||
"""Handle form submission"""
|
"""Handle form submission"""
|
||||||
try:
|
try:
|
||||||
template = get_object_or_404(FormTemplate, id=template_id)
|
template = get_object_or_404(FormTemplate, id=template_id)
|
||||||
print(template)
|
|
||||||
|
|
||||||
# Create form submission
|
# # Create form submission
|
||||||
submission = FormSubmission.objects.create(
|
# print({key: value for key, value in request.POST.items()})
|
||||||
template=template,
|
# first_name = next((value for key, value in request.POST.items() if key == 'First Name'), None)
|
||||||
applicant_name=request.POST.get('applicant_name', ''),
|
# last_name = next((value for key, value in request.POST.items() if key == 'Last Name'), None)
|
||||||
applicant_email=request.POST.get('applicant_email', '')
|
# 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
|
# Process field responses
|
||||||
for field_id, value in request.POST.items():
|
for field_id, value in request.POST.items():
|
||||||
if field_id.startswith('field_'):
|
if field_id.startswith('field_'):
|
||||||
@ -806,7 +807,29 @@ def submit_form(request, template_id):
|
|||||||
)
|
)
|
||||||
except FormField.DoesNotExist:
|
except FormField.DoesNotExist:
|
||||||
continue
|
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({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': 'Form submitted successfully!',
|
'message': 'Form submitted successfully!',
|
||||||
@ -818,16 +841,32 @@ def submit_form(request, template_id):
|
|||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=400)
|
}, 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"""
|
"""Display detailed view of a specific form submission"""
|
||||||
# Get the form template and verify ownership
|
# 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
|
# 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
|
# 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
|
# Get all responses for this submission, ordered by field order
|
||||||
responses = submission.responses.select_related('field').order_by('field__order')
|
responses = submission.responses.select_related('field').order_by('field__order')
|
||||||
@ -839,22 +878,21 @@ def form_submission_details(request, form_id, submission_id):
|
|||||||
'stage': stage,
|
'stage': stage,
|
||||||
'responses': responses.filter(field__stage=stage)
|
'responses': responses.filter(field__stage=stage)
|
||||||
}
|
}
|
||||||
# print(stages)
|
|
||||||
return render(request, 'forms/form_submission_details.html', {
|
return render(request, 'forms/form_submission_details.html', {
|
||||||
'form': form,
|
'template': template,
|
||||||
'submission': submission,
|
'submission': submission,
|
||||||
'stages': stages,
|
'stages': stages,
|
||||||
'responses': responses,
|
'responses': responses,
|
||||||
'stage_responses': stage_responses
|
'stage_responses': stage_responses
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def schedule_interviews_view(request, job_id):
|
||||||
|
job = get_object_or_404(Job, id=job_id)
|
||||||
def schedule_interviews_view(request, slug):
|
|
||||||
job = get_object_or_404(JobPosting, slug=slug)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
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
|
# Check if this is a confirmation request
|
||||||
if 'confirm_schedule' in request.POST:
|
if 'confirm_schedule' in request.POST:
|
||||||
@ -862,7 +900,7 @@ def schedule_interviews_view(request, slug):
|
|||||||
schedule_data = request.session.get('interview_schedule_data')
|
schedule_data = request.session.get('interview_schedule_data')
|
||||||
if not schedule_data:
|
if not schedule_data:
|
||||||
messages.error(request, "Session expired. Please try again.")
|
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
|
# Create the interview schedule
|
||||||
schedule = InterviewSchedule.objects.create(
|
schedule = InterviewSchedule.objects.create(
|
||||||
@ -875,6 +913,15 @@ def schedule_interviews_view(request, slug):
|
|||||||
candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids'])
|
candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids'])
|
||||||
schedule.candidates.set(candidates)
|
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
|
# Schedule the interviews
|
||||||
try:
|
try:
|
||||||
scheduled_count = schedule_interviews(schedule)
|
scheduled_count = schedule_interviews(schedule)
|
||||||
@ -885,16 +932,16 @@ def schedule_interviews_view(request, slug):
|
|||||||
# Clear the session data
|
# Clear the session data
|
||||||
if 'interview_schedule_data' in request.session:
|
if 'interview_schedule_data' in request.session:
|
||||||
del request.session['interview_schedule_data']
|
del request.session['interview_schedule_data']
|
||||||
return redirect('job_detail', slug=slug)
|
return redirect('job_detail', pk=job_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
f"Error scheduling interviews: {str(e)}"
|
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
|
# This is the initial form submission
|
||||||
if form.is_valid():
|
if form.is_valid() and break_formset.is_valid():
|
||||||
# Get the form data
|
# Get the form data
|
||||||
candidates = form.cleaned_data['candidates']
|
candidates = form.cleaned_data['candidates']
|
||||||
start_date = form.cleaned_data['start_date']
|
start_date = form.cleaned_data['start_date']
|
||||||
@ -902,11 +949,18 @@ def schedule_interviews_view(request, slug):
|
|||||||
working_days = form.cleaned_data['working_days']
|
working_days = form.cleaned_data['working_days']
|
||||||
start_time = form.cleaned_data['start_time']
|
start_time = form.cleaned_data['start_time']
|
||||||
end_time = form.cleaned_data['end_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']
|
interview_duration = form.cleaned_data['interview_duration']
|
||||||
buffer_time = form.cleaned_data['buffer_time']
|
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)
|
# Create a temporary schedule object (not saved to DB)
|
||||||
temp_schedule = InterviewSchedule(
|
temp_schedule = InterviewSchedule(
|
||||||
job=job,
|
job=job,
|
||||||
@ -915,14 +969,20 @@ def schedule_interviews_view(request, slug):
|
|||||||
working_days=working_days,
|
working_days=working_days,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
break_start_time=break_start_time,
|
|
||||||
break_end_time=break_end_time,
|
|
||||||
interview_duration=interview_duration,
|
interview_duration=interview_duration,
|
||||||
buffer_time=buffer_time
|
buffer_time=buffer_time
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create temporary break time objects
|
||||||
|
temp_breaks = []
|
||||||
|
for break_data in breaks:
|
||||||
|
temp_breaks.append(BreakTime(
|
||||||
|
start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(),
|
||||||
|
end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time()
|
||||||
|
))
|
||||||
|
|
||||||
# Get available slots
|
# 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):
|
if len(available_slots) < len(candidates):
|
||||||
messages.error(
|
messages.error(
|
||||||
@ -931,6 +991,7 @@ def schedule_interviews_view(request, slug):
|
|||||||
)
|
)
|
||||||
return render(request, 'interviews/schedule_interviews.html', {
|
return render(request, 'interviews/schedule_interviews.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'break_formset': break_formset,
|
||||||
'job': job
|
'job': job
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -951,11 +1012,10 @@ def schedule_interviews_view(request, slug):
|
|||||||
'working_days': working_days,
|
'working_days': working_days,
|
||||||
'start_time': start_time.isoformat(),
|
'start_time': start_time.isoformat(),
|
||||||
'end_time': end_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,
|
'interview_duration': interview_duration,
|
||||||
'buffer_time': buffer_time,
|
'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
|
request.session['interview_schedule_data'] = schedule_data
|
||||||
|
|
||||||
@ -968,15 +1028,16 @@ def schedule_interviews_view(request, slug):
|
|||||||
'working_days': working_days,
|
'working_days': working_days,
|
||||||
'start_time': start_time,
|
'start_time': start_time,
|
||||||
'end_time': end_time,
|
'end_time': end_time,
|
||||||
'break_start_time': break_start_time,
|
'breaks': breaks,
|
||||||
'break_end_time': break_end_time,
|
|
||||||
'interview_duration': interview_duration,
|
'interview_duration': interview_duration,
|
||||||
'buffer_time': buffer_time
|
'buffer_time': buffer_time
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
form = InterviewScheduleForm(slug=slug)
|
form = InterviewScheduleForm(job_id=job_id)
|
||||||
|
break_formset = BreakTimeFormSet()
|
||||||
|
|
||||||
return render(request, 'interviews/schedule_interviews.html', {
|
return render(request, 'interviews/schedule_interviews.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'break_formset': break_formset,
|
||||||
'job': job
|
'job': job
|
||||||
})
|
})
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
|
{% load partials %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
||||||
<head>
|
<head>
|
||||||
@ -470,6 +471,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<main class="container flex-grow-1">
|
<main class="container flex-grow-1">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
@ -479,11 +481,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="footer mt-auto">
|
<footer class="footer mt-auto">
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<p class="mb-0">
|
<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' %}
|
{% extends 'base.html' %}
|
||||||
{% load static i18n crispy_forms_tags %}
|
{% load static i18n crispy_forms_tags %}
|
||||||
|
{% load partials %}
|
||||||
|
|
||||||
{% block title %}Form Templates - ATS{% endblock %}
|
{% block title %}Form Templates - ATS{% endblock %}
|
||||||
|
|
||||||
@ -229,7 +230,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% if templates %}
|
{% if templates %}
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
{% for template in templates %}
|
{% for template in templates %}
|
||||||
@ -276,6 +276,9 @@
|
|||||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
<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" %}
|
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||||
</a>
|
</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"
|
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
||||||
data-template-id="{{ template.id }}"
|
data-template-id="{{ template.id }}"
|
||||||
data-template-name="{{ template.name }}">
|
data-template-name="{{ template.name }}">
|
||||||
@ -337,6 +340,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'includes/delete_modal.html' %}
|
{% include 'includes/delete_modal.html' %}
|
||||||
@ -352,6 +356,7 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
{% url 'create_form_template' as create_form_template_url %}
|
||||||
<form id="createTemplateForm" method="post" action="{% url 'create_form_template' %}">
|
<form id="createTemplateForm" method="post" action="{% url 'create_form_template' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{form|crispy}}
|
{{form|crispy}}
|
||||||
@ -367,185 +372,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
|||||||
@ -29,6 +29,28 @@
|
|||||||
--radius: 16px; /* Increased radius for a softer look */
|
--radius: 16px; /* Increased radius for a softer look */
|
||||||
--transition: all 0.3s ease;
|
--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;
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* Remove centering/flex properties to allow for normal document flow and scrolling */
|
/* Remove centering/flex properties to allow for normal document flow and scrolling */
|
||||||
@ -309,6 +331,16 @@
|
|||||||
.option-item input[type="checkbox"]:checked {
|
.option-item input[type="checkbox"]:checked {
|
||||||
accent-color: var(--primary);
|
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="checkbox"]:checked {
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
.option-item input {
|
.option-item input {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
@ -801,8 +833,8 @@
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// Add applicant info
|
// Add applicant info
|
||||||
formData.append('applicant_name', state.formData.applicant_name || '');
|
//formData.append('applicant_name', state.formData.applicant_name || '');
|
||||||
formData.append('applicant_email', state.formData.applicant_email || '');
|
//formData.append('applicant_email', state.formData.applicant_email || '');
|
||||||
|
|
||||||
// Add field responses
|
// Add field responses
|
||||||
state.stages.forEach(stage => {
|
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>
|
<p><strong>Working Hours:</strong> {{ start_time|time:"g:i A" }} to {{ end_time|time:"g:i A" }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if break_start_time and break_end_time %}
|
{% if breaks %}
|
||||||
<p><strong>Break Time:</strong> {{ break_start_time|time:"g:i A" }} to {{ break_end_time|time:"g:i A" }}</p>
|
<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 %}
|
{% endif %}
|
||||||
<p><strong>Interview Duration:</strong> {{ interview_duration }} minutes</p>
|
<p><strong>Interview Duration:</strong> {{ interview_duration }} minutes</p>
|
||||||
<p><strong>Buffer Time:</strong> {{ buffer_time }} 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">
|
<button type="submit" name="confirm_schedule" class="btn btn-success">
|
||||||
<i class="fas fa-check"></i> Confirm Schedule
|
<i class="fas fa-check"></i> Confirm Schedule
|
||||||
</button>
|
</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
|
<i class="fas fa-arrow-left"></i> Back to Edit
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
@ -109,13 +114,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% 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) {
|
eventClick: function(info) {
|
||||||
// Show candidate details in a modal or alert
|
// Show candidate details in a modal or alert
|
||||||
|
if (info.event.title !== 'Break') {
|
||||||
alert('Candidate: ' + info.event.title +
|
alert('Candidate: ' + info.event.title +
|
||||||
'\nDate: ' + info.event.start.toLocaleDateString() +
|
'\nDate: ' + info.event.start.toLocaleDateString() +
|
||||||
'\nTime: ' + info.event.extendedProps.time +
|
'\nTime: ' + info.event.extendedProps.time +
|
||||||
'\nEmail: ' + info.event.extendedProps.email);
|
'\nEmail: ' + info.event.extendedProps.email);
|
||||||
|
}
|
||||||
info.jsEvent.preventDefault();
|
info.jsEvent.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post" id="schedule-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -21,31 +21,31 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h5>Schedule Details</h5>
|
<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>
|
<label for="{{ form.start_date.id_for_label }}">Start Date</label>
|
||||||
{{ form.start_date }}
|
{{ form.start_date }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group mb-3">
|
||||||
<label for="{{ form.end_date.id_for_label }}">End Date</label>
|
<label for="{{ form.end_date.id_for_label }}">End Date</label>
|
||||||
{{ form.end_date }}
|
{{ form.end_date }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group mb-3">
|
||||||
<label>Working Days</label>
|
<label>Working Days</label>
|
||||||
{{ form.working_days }}
|
{{ form.working_days }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<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>
|
<label for="{{ form.start_time.id_for_label }}">Start Time</label>
|
||||||
{{ form.start_time }}
|
{{ form.start_time }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<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>
|
<label for="{{ form.end_time.id_for_label }}">End Time</label>
|
||||||
{{ form.end_time }}
|
{{ form.end_time }}
|
||||||
</div>
|
</div>
|
||||||
@ -54,30 +54,14 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group mb-3">
|
||||||
<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">
|
|
||||||
<label for="{{ form.interview_duration.id_for_label }}">Interview Duration (minutes)</label>
|
<label for="{{ form.interview_duration.id_for_label }}">Interview Duration (minutes)</label>
|
||||||
{{ form.interview_duration }}
|
{{ form.interview_duration }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<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>
|
<label for="{{ form.buffer_time.id_for_label }}">Buffer Time (minutes)</label>
|
||||||
{{ form.buffer_time }}
|
{{ form.buffer_time }}
|
||||||
</div>
|
</div>
|
||||||
@ -86,12 +70,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">Schedule Interviews</button>
|
<button type="submit" class="btn btn-primary">Preview Schedule</button>
|
||||||
<a href="{% url 'job_detail' slug=job.slug %}" class="btn btn-secondary">Cancel</a>
|
<a href="{% url 'job_detail' pk=job.id %}" class="btn btn-secondary">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
@ -151,7 +151,7 @@
|
|||||||
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
|
<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 "Phone" %}</th>
|
||||||
<th scope="col" style="width: 15%;">{% trans "Job" %}</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%;">{% trans "Created" %}</th>
|
||||||
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -164,8 +164,8 @@
|
|||||||
<td>{{ candidate.phone }}</td>
|
<td>{{ candidate.phone }}</td>
|
||||||
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge {% if candidate.applied %}bg-success{% else %}bg-warning{% endif %}">
|
<span class="badge bg-primary">
|
||||||
{{ candidate.applied|yesno:"Yes,No" }}
|
{{ candidate.stage }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user