more update related to integrations

This commit is contained in:
ismail 2025-10-09 12:55:09 +03:00
parent 41cf8ea28a
commit 579cc085e2
50 changed files with 1322 additions and 2008 deletions

View File

@ -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,24 +439,23 @@ 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
) )
class Meta: class Meta:
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

View File

@ -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')),
], ],
), ),
] ]

View 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,
),
]

View File

@ -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)),
],
),
]

View File

@ -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),
),
]

View 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'),
),
]

View File

@ -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),
),
]

View File

@ -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)),
],
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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')),
],
),
]

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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 = [
]

View File

@ -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',
),
]

View File

@ -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'],
},
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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,
},
),
]

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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'),

View File

@ -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
conflict_with_break = True if not (current_time >= break_time.end_time or slot_end_time <= break_time.start_time):
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_start}-{break_end}") conflict_with_break = True
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_time.start_time}-{break_time.end_time}")
break
if not conflict_with_break: if not conflict_with_break:
# Add this slot to available slots # Add this slot to available slots

View File

@ -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
}) })

View File

@ -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,20 +471,19 @@
</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 %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</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">

View 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 %}

View File

@ -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 %}

View File

@ -7,6 +7,28 @@
<title>{% translate "Application Form" %}</title> <title>{% translate "Application Form" %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* KAAT-S Theme Variables */
:root {
--kaauh-teal: #00636e; /* Main Primary Color */
--kaauh-teal-dark: #004a53; /* Dark Primary Color */
/* Mapping wizard defaults to theme colors */
--primary: var(--kaauh-teal);
--primary-light: #007c89; /* Slightly lighter shade for subtle hover/border */
--secondary: var(--kaauh-teal-dark);
--success: #198754; /* Keeping a standard success green for Submit */
--error: #dc3545; /* Standard danger red */
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--light-gray: #e9ecef;
--border: #dee2e6;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--radius: 16px; /* Increased radius for a softer look */
--transition: all 0.3s ease;
}
<style> <style>
/* KAAT-S Theme Variables */ /* KAAT-S Theme Variables */
:root { :root {
@ -299,6 +321,16 @@
background: rgba(0, 99, 110, 0.05); /* Light teal background */ background: rgba(0, 99, 110, 0.05); /* Light teal background */
} }
.option-item.error {
border-color: var(--error);
background: rgba(220, 53, 69, 0.05);
}
/* Ensures radio/checkbox controls themselves use the primary color */
.option-item input[type="radio"]:checked,
.option-item input[type="checkbox"]:checked {
accent-color: var(--primary);
}
.option-item.error { .option-item.error {
border-color: var(--error); border-color: var(--error);
background: rgba(220, 53, 69, 0.05); background: rgba(220, 53, 69, 0.05);
@ -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 => {

View File

@ -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
alert('Candidate: ' + info.event.title + if (info.event.title !== 'Break') {
'\nDate: ' + info.event.start.toLocaleDateString() + alert('Candidate: ' + info.event.title +
'\nTime: ' + info.event.extendedProps.time + '\nDate: ' + info.event.start.toLocaleDateString() +
'\nEmail: ' + info.event.extendedProps.email); '\nTime: ' + info.event.extendedProps.time +
'\nEmail: ' + info.event.extendedProps.email);
}
info.jsEvent.preventDefault(); info.jsEvent.preventDefault();
} }
}); });

View File

@ -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>&nbsp;</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>&nbsp;</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 %}

View File

@ -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>