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 .validators import validate_hash_tags
from django.core.validators import URLValidator
from django.forms.formsets import formset_factory
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule,BreakTime
from django_summernote.widgets import SummernoteWidget
class CandidateForm(forms.ModelForm):
class Meta:
model = Candidate
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume', 'stage']
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume',]
labels = {
'first_name': _('First Name'),
'last_name': _('Last Name'),
'phone': _('Phone'),
'email': _('Email'),
'resume': _('Resume'),
'stage': _('Application Stage'),
}
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter first name')}),
@ -154,17 +154,17 @@ class TrainingMaterialForm(forms.ModelForm):
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter material title')}),
# 💡 Use SummernoteWidget here
'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}),
'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}),
'video_link': forms.URLInput(attrs={'class': 'form-control', 'placeholder': _('https://www.youtube.com/watch?v=...')}),
'file': forms.FileInput(attrs={'class': 'form-control'}),
}
# The __init__ and FormHelper layout remains the same
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'g-3'
self.helper.form_class = 'g-3'
self.helper.layout = Layout(
'title',
@ -175,7 +175,7 @@ class TrainingMaterialForm(forms.ModelForm):
css_class='g-3 mb-4'
),
Div(
Submit('submit', _('Create Material'),
Submit('submit', _('Create Material'),
css_class='btn btn-main-action'),
css_class='col-12 mt-4'
)
@ -261,7 +261,7 @@ class JobPostingForm(forms.ModelForm):
'application_instructions': SummernoteWidget(attrs={
'placeholder': 'Special instructions for applicants (e.g., required documents, reference requirements, etc.)',
}),
'open_positions': forms.NumberInput(attrs={
'class': 'form-control',
@ -412,11 +412,21 @@ class FormTemplateForm(forms.ModelForm):
Field('is_active', css_class='form-check-input'),
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
)
class BreakTimeForm(forms.ModelForm):
class Meta:
model = BreakTime
fields = ['start_time', 'end_time']
widgets = {
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
}
BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
class InterviewScheduleForm(forms.ModelForm):
candidates = forms.ModelMultipleChoiceField(
queryset=Candidate.objects.none(),
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
widget=forms.CheckboxSelectMultiple,
required=True
)
working_days = forms.MultipleChoiceField(
@ -429,24 +439,23 @@ class InterviewScheduleForm(forms.ModelForm):
(5, 'Saturday'),
(6, 'Sunday'),
],
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
widget=forms.CheckboxSelectMultiple,
required=True
)
)
class Meta:
model = InterviewSchedule
fields = [
'candidates', 'start_date', 'end_date', 'working_days',
'start_time', 'end_time', 'break_start_time', 'break_end_time',
'interview_duration', 'buffer_time'
'start_time', 'end_time', 'interview_duration', 'buffer_time'
]
widgets = {
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'break_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}),
'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}),
}
def __init__(self, slug, *args, **kwargs):
@ -456,11 +465,7 @@ class InterviewScheduleForm(forms.ModelForm):
job__slug=slug,
stage='Interview'
)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3'
self.helper.field_class = 'col-md-9'
def clean_working_days(self):
working_days = self.cleaned_data.get('working_days')
# Convert string values to integers

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_countries.fields
import django_extensions.db.fields
import recruitment.validators
from django.conf import settings
from django.db import migrations, models
@ -9,32 +14,375 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Job',
name='BreakTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description_en', models.TextField()),
('description_ar', models.TextField()),
('is_published', models.BooleanField(default=False)),
('posted_to_linkedin', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('start_time', models.TimeField(verbose_name='Start Time')),
('end_time', models.TimeField(verbose_name='End Time')),
],
),
migrations.CreateModel(
name='FormStage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='Name of the stage', max_length=200)),
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
],
options={
'verbose_name': 'Form Stage',
'verbose_name_plural': 'Form Stages',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='HiringAgency',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
('address', models.TextField(blank=True, null=True)),
],
options={
'verbose_name': 'Hiring Agency',
'verbose_name_plural': 'Hiring Agencies',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Source',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')),
('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')),
('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')),
('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')),
('created_at', models.DateTimeField(auto_now_add=True)),
('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')),
('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')),
('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')),
('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')),
('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')),
('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')),
('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')),
],
options={
'verbose_name': 'Source',
'verbose_name_plural': 'Sources',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ZoomMeeting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('topic', models.CharField(max_length=255, verbose_name='Topic')),
('meeting_id', models.CharField(max_length=20, unique=True, verbose_name='Meeting ID')),
('start_time', models.DateTimeField(verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration')),
('timezone', models.CharField(max_length=50, verbose_name='Timezone')),
('join_url', models.URLField(verbose_name='Join URL')),
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')),
('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')),
('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FormField',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('label', models.CharField(help_text='Label for the field', max_length=200)),
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')),
('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')),
('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')),
],
options={
'verbose_name': 'Form Field',
'verbose_name_plural': 'Form Fields',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='FormSubmission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('submitted_at', models.DateTimeField(auto_now_add=True)),
('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)),
('applicant_email', models.EmailField(blank=True, help_text='Email of the applicant', max_length=254)),
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Form Submission',
'verbose_name_plural': 'Form Submissions',
'ordering': ['-submitted_at'],
},
),
migrations.CreateModel(
name='FieldResponse',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
],
options={
'verbose_name': 'Field Response',
'verbose_name_plural': 'Field Responses',
},
),
migrations.CreateModel(
name='FormTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='Name of the form template', max_length=200)),
('description', models.TextField(blank=True, help_text='Description of the form template')),
('is_active', models.BooleanField(default=True, help_text='Whether this template is active')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Form Template',
'verbose_name_plural': 'Form Templates',
'ordering': ['-created_at'],
},
),
migrations.AddField(
model_name='formsubmission',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'),
),
migrations.AddField(
model_name='formstage',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
),
migrations.CreateModel(
name='Candidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('email', models.EmailField(max_length=254)),
('resume', models.FileField(upload_to='resumes/')),
('parsed_summary', models.TextField(blank=True)),
('status', models.CharField(default='Applied', max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone', models.CharField(max_length=20, verbose_name='Phone')),
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
('applied', models.BooleanField(default=False, verbose_name='Applied')),
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')),
('exam_date', models.DateField(blank=True, null=True, verbose_name='Exam Date')),
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')),
('interview_date', models.DateField(blank=True, null=True, verbose_name='Interview Date')),
('interview_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Interview Status')),
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')),
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
('match_score', models.IntegerField(blank=True, null=True)),
('strengths', models.TextField(blank=True)),
('weaknesses', models.TextField(blank=True)),
('criteria_checklist', models.JSONField(blank=True, default=dict)),
('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')),
],
options={
'verbose_name': 'Candidate',
'verbose_name_plural': 'Candidates',
},
),
migrations.CreateModel(
name='JobPosting',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=200)),
('department', models.CharField(blank=True, max_length=100)),
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
('location_city', models.CharField(blank=True, max_length=100)),
('location_state', models.CharField(blank=True, max_length=100)),
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
('description', models.TextField(help_text='Full job description including responsibilities and requirements')),
('qualifications', models.TextField(blank=True, help_text='Required qualifications and skills')),
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
('benefits', models.TextField(blank=True, help_text='Benefits offered')),
('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])),
('application_deadline', models.DateField(blank=True, null=True)),
('application_instructions', models.TextField(blank=True, help_text='Special instructions for applicants')),
('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
('status', models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True)),
('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])),
('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)),
('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')),
('posted_to_linkedin', models.BooleanField(default=False)),
('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)),
('linkedin_posted_at', models.DateTimeField(blank=True, null=True)),
('published_at', models.DateTimeField(blank=True, null=True)),
('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)),
('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)),
('start_date', models.DateField(blank=True, help_text='Desired start date', null=True)),
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')),
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
],
options={
'verbose_name': 'Job Posting',
'verbose_name_plural': 'Job Postings',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='InterviewSchedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('start_date', models.DateField(verbose_name='Start Date')),
('end_date', models.DateField(verbose_name='End Date')),
('working_days', models.JSONField(verbose_name='Working Days')),
('start_time', models.TimeField(verbose_name='Start Time')),
('end_time', models.TimeField(verbose_name='End Time')),
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
('created_at', models.DateTimeField(auto_now_add=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.job')),
('breaks', models.ManyToManyField(blank=True, related_name='schedules', to='recruitment.breaktime')),
('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='formtemplate',
name='job',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
),
migrations.AddField(
model_name='candidate',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
),
migrations.CreateModel(
name='SharedFormTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
],
options={
'verbose_name': 'Shared Form Template',
'verbose_name_plural': 'Shared Form Templates',
},
),
migrations.CreateModel(
name='IntegrationLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')),
('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')),
('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')),
('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')),
('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')),
('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')),
('error_message', models.TextField(blank=True, verbose_name='Error Message')),
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')),
('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')),
],
options={
'verbose_name': 'Integration Log',
'verbose_name_plural': 'Integration Logs',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TrainingMaterial',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=255, verbose_name='Title')),
('content', models.TextField(blank=True, verbose_name='Content')),
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
],
options={
'verbose_name': 'Training Material',
'verbose_name_plural': 'Training Materials',
},
),
migrations.CreateModel(
name='ScheduledInterview',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('interview_date', models.DateField(verbose_name='Interview Date')),
('interview_time', models.TimeField(verbose_name='Interview Time')),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
],
),
]

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)
def score_candidate_resume(sender, instance, created, **kwargs):
# Skip if no resume or OpenRouter not configured
if instance.resume:
return
if kwargs.get('update_fields') is not None:
return
# Optional: Only re-score if resume changed (advanced: track file hash)
# For simplicity, we score on every save with a resume
try:
# Get absolute file path
file_path = instance.resume.path
@ -144,7 +135,7 @@ def create_default_stages(sender, instance, created, **kwargs):
"""
Create default resume stages when a new FormTemplate is created
"""
if created: # Only run for new templates, not updates
if created:
with transaction.atomic():
# Stage 1: Contact Information
contact_stage = FormStage.objects.create(
@ -155,18 +146,26 @@ def create_default_stages(sender, instance, created, **kwargs):
)
FormField.objects.create(
stage=contact_stage,
label='Full Name',
label='First Name',
field_type='text',
required=True,
order=0,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Last Name',
field_type='text',
required=True,
order=1,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Email Address',
field_type='email',
required=True,
order=1,
order=2,
is_predefined=True
)
FormField.objects.create(
@ -174,7 +173,7 @@ def create_default_stages(sender, instance, created, **kwargs):
label='Phone Number',
field_type='phone',
required=True,
order=2,
order=3,
is_predefined=True
)
FormField.objects.create(
@ -182,7 +181,7 @@ def create_default_stages(sender, instance, created, **kwargs):
label='Address',
field_type='text',
required=False,
order=3,
order=4,
is_predefined=True
)
FormField.objects.create(
@ -190,10 +189,10 @@ def create_default_stages(sender, instance, created, **kwargs):
label='Resume Upload',
field_type='file',
required=True,
order=4,
order=5,
is_predefined=True,
file_types='.pdf,.doc,.docx',
max_file_size=5
max_file_size=1
)
# Stage 2: Resume Objective

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>/submit/', views.submit_form, name='submit_form'),
path('forms/<int:form_id>/submissions/<int:submission_id>/', views.form_submission_details, name='form_submission_details'),
path('forms/<int:form_id>/submissions/<int:s>/', views.form_submission_details, name='form_submission_details'),
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
path('api/templates/', views.list_form_templates, name='list_form_templates'),
path('api/templates/save/', views.save_form_template, name='save_form_template'),

View File

@ -465,7 +465,7 @@ def send_interview_email(scheduled_interview):
fail_silently=False,
)
def get_available_time_slots(schedule):
def get_available_time_slots(schedule, breaks=None):
"""
Generate a list of available time slots based on the schedule criteria.
Returns a list of dictionaries with 'date' and 'time' keys.
@ -481,8 +481,6 @@ def get_available_time_slots(schedule):
# Parse times
start_time = schedule.start_time
end_time = schedule.end_time
break_start = schedule.break_start_time
break_end = schedule.break_end_time
# Calculate slot duration (interview duration + buffer time)
slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time)
@ -492,6 +490,7 @@ def get_available_time_slots(schedule):
print(f"Date range: {current_date} to {end_date}")
print(f"Time range: {start_time} to {end_time}")
print(f"Slot duration: {slot_duration}")
print(f"Breaks: {breaks}")
while current_date <= end_date:
# Check if current day is a working day
@ -510,13 +509,15 @@ def get_available_time_slots(schedule):
if slot_end_time > end_time:
break
# Check if slot conflicts with break time
# Check if slot conflicts with any break time
conflict_with_break = False
if break_start and break_end:
# Check if the slot overlaps with break time
if not (current_time >= break_end or slot_end_time <= break_start):
conflict_with_break = True
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_start}-{break_end}")
if breaks:
for break_time in breaks:
# Check if the slot overlaps with this break time
if not (current_time >= break_time.end_time or slot_end_time <= break_time.start_time):
conflict_with_break = True
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_time.start_time}-{break_time.end_time}")
break
if not conflict_with_break:
# Add this slot to available slots

View File

@ -1,6 +1,7 @@
import json
import requests
from rich import print
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.http import JsonResponse
@ -10,19 +11,23 @@ from django.db.models import Q
from django.urls import reverse
from django.conf import settings
from django.utils import timezone
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm,BreakTimeFormSet
from rest_framework import viewsets
from django.contrib import messages
from django.core.paginator import Paginator
from .linkedin_service import LinkedInService
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule
from .models import ZoomMeeting, Candidate, JobPosting
from .serializers import JobPostingSerializer, CandidateSerializer
from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import CreateView,UpdateView,DetailView,ListView
from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,schedule_interviews,get_available_time_slots
from django.views.decorators.csrf import ensure_csrf_cookie
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule,BreakTime, ZoomMeeting, Candidate, JobPosting
import logging
from datastar_py.django import (
DatastarResponse,
ServerSentEventGenerator as SSE,
read_signals,
)
logger=logging.getLogger(__name__)
@ -194,7 +199,6 @@ def create_job(request):
def edit_job(request,slug):
"""Edit an existing job posting"""
if request.method=='POST':
@ -575,14 +579,8 @@ def applicant_job_detail(request,slug):
# submissions = form.submissions.all().order_by('-submitted_at')
# # Pagination
# paginator = Paginator(submissions, 20)
# page_number = request.GET.get('page')
# page_obj = paginator.get_page(page_number)
#
# return render(request, 'forms/form_submissions.html', {
# 'form': form,
# 'page_obj': page_obj
# })
@ensure_csrf_cookie
@ -732,9 +730,8 @@ def create_form_template(request):
template = form.save(commit=False)
template.created_by = request.user
template.save()
messages.success(request, f'Form template "{template.name}" created successfully!')
return redirect('form_builder', template_id=template.id)
return redirect('form_templates_list')
else:
form = FormTemplateForm()
@ -770,15 +767,19 @@ def submit_form(request, template_id):
"""Handle form submission"""
try:
template = get_object_or_404(FormTemplate, id=template_id)
print(template)
# Create form submission
submission = FormSubmission.objects.create(
template=template,
applicant_name=request.POST.get('applicant_name', ''),
applicant_email=request.POST.get('applicant_email', '')
)
# # Create form submission
# print({key: value for key, value in request.POST.items()})
# first_name = next((value for key, value in request.POST.items() if key == 'First Name'), None)
# last_name = next((value for key, value in request.POST.items() if key == 'Last Name'), None)
# email = next((value for key, value in request.POST.items() if key == 'Email Address'), None)
# phone = next((value for key, value in request.POST.items() if key == 'Phone Number'), None)
# address = next((value for key, value in request.POST.items() if key == 'Address'), None)
# resume = next((value for key, value in request.POST.items() if key == 'Resume Upload'), None)
# print(first_name, last_name, email, phone, address, resume)
# create candidate
submission = FormSubmission.objects.create(template=template)
# Process field responses
for field_id, value in request.POST.items():
if field_id.startswith('field_'):
@ -806,7 +807,29 @@ def submit_form(request, template_id):
)
except FormField.DoesNotExist:
continue
try:
first_name = submission.responses.get(field__label="First Name")
last_name = submission.responses.get(field__label="Last Name")
email = submission.responses.get(field__label="Email Address")
phone = submission.responses.get(field__label="Phone Number")
address = submission.responses.get(field__label="Address")
resume = submission.responses.get(field__label="Resume Upload")
submission.applicant_name = f"{first_name.display_value} {last_name.display_value}"
submission.applicant_email = email.display_value
submission.save()
Candidate.objects.create(
first_name=first_name.display_value,
last_name=last_name.display_value,
email=email.display_value,
phone=phone.display_value,
address=address.display_value,
resume=resume.get_file if resume.is_file else None,
job=submission.template.job
)
except Exception as e:
logger.error(f"Candidate creation failed,{e}")
pass
return JsonResponse({
'success': True,
'message': 'Form submitted successfully!',
@ -818,16 +841,32 @@ def submit_form(request, template_id):
'error': str(e)
}, status=400)
def form_submission_details(request, form_id, submission_id):
def form_template_submissions_list(request, template_slug):
"""List all submissions for a specific form template"""
template = get_object_or_404(FormTemplate, slug=template_slug, created_by=request.user)
submissions = FormSubmission.objects.filter(template=template).order_by('-submitted_at')
# Pagination
paginator = Paginator(submissions, 10) # Show 10 submissions per page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'forms/form_template_submissions_list.html', {
'template': template,
'page_obj': page_obj
})
def form_submission_details(request, template_id, submission_id):
"""Display detailed view of a specific form submission"""
# Get the form template and verify ownership
form = get_object_or_404(FormTemplate, id=form_id, created_by=request.user)
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
# Get the specific submission
submission = get_object_or_404(FormSubmission, id=submission_id, template=form)
submission = get_object_or_404(FormSubmission, id=submission_id, template=template)
# Get all stages with their fields
stages = form.stages.prefetch_related('fields').order_by('order')
stages = template.stages.prefetch_related('fields').order_by('order')
# Get all responses for this submission, ordered by field order
responses = submission.responses.select_related('field').order_by('field__order')
@ -839,22 +878,21 @@ def form_submission_details(request, form_id, submission_id):
'stage': stage,
'responses': responses.filter(field__stage=stage)
}
# print(stages)
return render(request, 'forms/form_submission_details.html', {
'form': form,
'template': template,
'submission': submission,
'stages': stages,
'responses': responses,
'stage_responses': stage_responses
})
def schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
def schedule_interviews_view(request, job_id):
job = get_object_or_404(Job, id=job_id)
if request.method == 'POST':
form = InterviewScheduleForm(slug, request.POST)
form = InterviewScheduleForm(job_id, request.POST)
break_formset = BreakTimeFormSet(request.POST)
# Check if this is a confirmation request
if 'confirm_schedule' in request.POST:
@ -862,7 +900,7 @@ def schedule_interviews_view(request, slug):
schedule_data = request.session.get('interview_schedule_data')
if not schedule_data:
messages.error(request, "Session expired. Please try again.")
return redirect('schedule_interviews', slug=slug)
return redirect('schedule_interviews', job_id=job_id)
# Create the interview schedule
schedule = InterviewSchedule.objects.create(
@ -875,6 +913,15 @@ def schedule_interviews_view(request, slug):
candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids'])
schedule.candidates.set(candidates)
# Add break times to the schedule
if 'breaks' in schedule_data and schedule_data['breaks']:
for break_data in schedule_data['breaks']:
break_time = BreakTime.objects.create(
start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(),
end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time()
)
schedule.breaks.add(break_time)
# Schedule the interviews
try:
scheduled_count = schedule_interviews(schedule)
@ -885,16 +932,16 @@ def schedule_interviews_view(request, slug):
# Clear the session data
if 'interview_schedule_data' in request.session:
del request.session['interview_schedule_data']
return redirect('job_detail', slug=slug)
return redirect('job_detail', pk=job_id)
except Exception as e:
messages.error(
request,
f"Error scheduling interviews: {str(e)}"
)
return redirect('schedule_interviews', slug=slug)
return redirect('schedule_interviews', job_id=job_id)
# This is the initial form submission
if form.is_valid():
if form.is_valid() and break_formset.is_valid():
# Get the form data
candidates = form.cleaned_data['candidates']
start_date = form.cleaned_data['start_date']
@ -902,11 +949,18 @@ def schedule_interviews_view(request, slug):
working_days = form.cleaned_data['working_days']
start_time = form.cleaned_data['start_time']
end_time = form.cleaned_data['end_time']
break_start_time = form.cleaned_data['break_start_time']
break_end_time = form.cleaned_data['break_end_time']
interview_duration = form.cleaned_data['interview_duration']
buffer_time = form.cleaned_data['buffer_time']
# Process break times
breaks = []
for break_form in break_formset:
if break_form.cleaned_data and not break_form.cleaned_data.get('DELETE'):
breaks.append({
'start_time': break_form.cleaned_data['start_time'].isoformat(),
'end_time': break_form.cleaned_data['end_time'].isoformat()
})
# Create a temporary schedule object (not saved to DB)
temp_schedule = InterviewSchedule(
job=job,
@ -915,14 +969,20 @@ def schedule_interviews_view(request, slug):
working_days=working_days,
start_time=start_time,
end_time=end_time,
break_start_time=break_start_time,
break_end_time=break_end_time,
interview_duration=interview_duration,
buffer_time=buffer_time
)
# Create temporary break time objects
temp_breaks = []
for break_data in breaks:
temp_breaks.append(BreakTime(
start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(),
end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time()
))
# Get available slots
available_slots = get_available_time_slots(temp_schedule)
available_slots = get_available_time_slots(temp_schedule, temp_breaks)
if len(available_slots) < len(candidates):
messages.error(
@ -931,6 +991,7 @@ def schedule_interviews_view(request, slug):
)
return render(request, 'interviews/schedule_interviews.html', {
'form': form,
'break_formset': break_formset,
'job': job
})
@ -951,11 +1012,10 @@ def schedule_interviews_view(request, slug):
'working_days': working_days,
'start_time': start_time.isoformat(),
'end_time': end_time.isoformat(),
'break_start_time': break_start_time.isoformat() if break_start_time else None,
'break_end_time': break_end_time.isoformat() if break_end_time else None,
'interview_duration': interview_duration,
'buffer_time': buffer_time,
'candidate_ids': [c.id for c in candidates]
'candidate_ids': [c.id for c in candidates],
'breaks': breaks
}
request.session['interview_schedule_data'] = schedule_data
@ -968,15 +1028,16 @@ def schedule_interviews_view(request, slug):
'working_days': working_days,
'start_time': start_time,
'end_time': end_time,
'break_start_time': break_start_time,
'break_end_time': break_end_time,
'breaks': breaks,
'interview_duration': interview_duration,
'buffer_time': buffer_time
})
else:
form = InterviewScheduleForm(slug=slug)
form = InterviewScheduleForm(job_id=job_id)
break_formset = BreakTimeFormSet()
return render(request, 'interviews/schedule_interviews.html', {
'form': form,
'break_formset': break_formset,
'job': job
})
})

View File

@ -1,4 +1,5 @@
{% load i18n static %}
{% load partials %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
<head>
@ -99,7 +100,7 @@
background-color: var(--kaauh-light-bg);
color: var(--kaauh-teal-dark);
}
/* Language Toggle Button Style */
.language-toggle-btn {
color: white !important;
@ -470,20 +471,19 @@
</div>
</nav>
<main class="container flex-grow-1">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</main>
{% endif %}
{% block content %}
{% endblock %}
</main>
<footer class="footer mt-auto">
<div class="container text-center">
<p class="mb-0">

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' %}
{% load static i18n crispy_forms_tags %}
{% load partials %}
{% block title %}Form Templates - ATS{% endblock %}
@ -229,7 +230,6 @@
</div>
</div>
{% if templates %}
<div class="row g-4">
{% for template in templates %}
@ -276,6 +276,9 @@
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
</a>
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm action-btn">
<i class="fas fa-file-alt me-1"></i> {% trans "Submissions" %}
</a>
<button class="btn btn-outline-danger btn-sm action-btn delete"
data-template-id="{{ template.id }}"
data-template-name="{{ template.name }}">
@ -337,6 +340,7 @@
</div>
</div>
{% endif %}
</div>
{% include 'includes/delete_modal.html' %}
@ -352,6 +356,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% url 'create_form_template' as create_form_template_url %}
<form id="createTemplateForm" method="post" action="{% url 'create_form_template' %}">
{% csrf_token %}
{{form|crispy}}
@ -366,186 +371,4 @@
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// JS logic remains the same as previous versions but ensures Django variables are handled
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
document.body.appendChild(toastContainer);
}
function createToast(message, type = 'success') {
const toastId = 'toast-' + Date.now();
const iconClass = type === 'success' ? 'check-circle text-success' : 'exclamation-circle text-danger';
const title = type === 'success' ? 'Success' : 'Error';
const toastHtml = `
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000">
<div class="toast-header">
<i class="fas fa-${iconClass} me-2"></i>
<strong class="me-auto">${title}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
// Search functionality - handles submission on Enter key
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const query = this.value;
// Assumes 'form_templates_list' is the view name for this page
window.location.href = query ? `?q=${encodeURIComponent(query)}` : '{% url "form_templates_list" %}';
}
});
// Bind search form submit to the main button click event for consistency
document.querySelector('.filter-buttons button[type="submit"]').addEventListener('click', function(e) {
// Prevent default submission to handle URL construction correctly
e.preventDefault();
const query = document.getElementById('searchInput').value;
window.location.href = query ? `?q=${encodeURIComponent(query)}` : '{% url "form_templates_list" %}';
});
// Delete modal functionality
let templateToDelete = null;
document.querySelectorAll('.delete').forEach(button => {
button.addEventListener('click', function() {
const templateId = this.dataset.templateId;
const templateName = this.dataset.templateName;
templateToDelete = templateId;
document.getElementById('templateNameToDelete').textContent = templateName;
deleteModal.show();
});
});
// Handle form submission in delete modal
document.getElementById('deleteForm').addEventListener('submit', async function(e) {
e.preventDefault();
if (!templateToDelete) return;
// This relies on 'csrfToken' being defined somewhere, which is typical for Django templates.
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
try {
// NOTE: Update this URL to match your actual Django API endpoint for deletion
const response = await fetch(`/api/templates/${templateToDelete}/delete/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
createToast(result.message);
deleteModal.hide();
// Smoothly remove the card
setTimeout(() => {
const buttonClicked = document.querySelector(`button[data-template-id="${templateToDelete}"]`);
if(buttonClicked) {
const cardToRemove = buttonClicked.closest('.col-lg-4');
if (cardToRemove) {
cardToRemove.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
cardToRemove.style.opacity = '0';
cardToRemove.style.transform = 'scale(0.8)';
setTimeout(() => {
cardToRemove.remove();
// Reload if the last card is removed to show the empty state
const remainingCards = document.querySelectorAll('.col-lg-4');
if (remainingCards.length === 0) {
window.location.reload();
}
}, 300);
}
}
}, 100);
} else {
createToast('Error: ' + (result.error || 'Could not delete template.'), 'error');
}
} catch (error) {
console.error('Error:', error);
createToast('An error occurred while deleting the template.', 'error');
}
templateToDelete = null;
});
// Handle modal close event
document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() {
templateToDelete = null;
});
// Handle create template form submission
document.getElementById('createTemplateForm').addEventListener('submit', async function(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
try {
const response = await fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
}
});
const result = await response.json();
if (response.ok && result.success) {
// Show success toast
createToast(result.message || 'Template created successfully!');
// Close modal
bootstrap.Modal.getInstance(document.getElementById('createTemplateModal')).hide();
// Clear form
form.reset();
// Redirect to form builder with new template ID
if (result.template_id) {
window.location.href = `{% url 'form_builder' %}${result.template_id}/`;
} else {
// Fallback to template list if no ID is returned
window.location.reload();
}
} else {
// Show error toast
createToast('Error: ' + (result.message || 'Could not create template.'), 'error');
}
} catch (error) {
console.error('Error:', error);
createToast('An error occurred while creating the template.', 'error');
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -7,19 +7,41 @@
<title>{% translate "Application Form" %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* KAAT-S Theme Variables */
:root {
--kaauh-teal: #00636e; /* Main Primary Color */
--kaauh-teal-dark: #004a53; /* Dark Primary Color */
/* Mapping wizard defaults to theme colors */
--primary: var(--kaauh-teal);
--primary-light: #007c89; /* Slightly lighter shade for subtle hover/border */
--secondary: var(--kaauh-teal-dark);
--success: #198754; /* Keeping a standard success green for Submit */
--error: #dc3545; /* Standard danger red */
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--light-gray: #e9ecef;
--border: #dee2e6;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--radius: 16px; /* Increased radius for a softer look */
--transition: all 0.3s ease;
}
<style>
/* KAAT-S Theme Variables */
:root {
--kaauh-teal: #00636e; /* Main Primary Color */
--kaauh-teal-dark: #004a53; /* Dark Primary Color */
/* Mapping wizard defaults to theme colors */
--primary: var(--kaauh-teal);
--primary-light: #007c89; /* Slightly lighter shade for subtle hover/border */
--secondary: var(--kaauh-teal-dark);
--success: #198754; /* Keeping a standard success green for Submit */
--error: #dc3545; /* Standard danger red */
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
@ -33,7 +55,7 @@
body {
/* Remove centering/flex properties to allow for normal document flow and scrolling */
padding-top: 56px; /* Space for the sticky navbar */
/* Dark gradient background to match the theme */
background: linear-gradient(135deg, var(--kaauh-teal-dark) 0%, #1e3a47 100%);
background-image: url("{% static 'image/vision.svg' %}");
@ -44,7 +66,7 @@
padding: 0; /* Remove padding from body */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Wrapper to center the wizard content below the navbar */
.page-content-wrapper {
display: flex;
@ -64,8 +86,8 @@
display: flex;
flex-direction: column;
/* Allow height to be determined by content, constrained by max-height */
height: auto;
max-height: 90vh;
height: auto;
max-height: 90vh;
}
/* Progress Bar */
@ -299,13 +321,23 @@
background: rgba(0, 99, 110, 0.05); /* Light teal background */
}
.option-item.error {
border-color: var(--error);
background: rgba(220, 53, 69, 0.05);
}
/* Ensures radio/checkbox controls themselves use the primary color */
.option-item input[type="radio"]:checked,
.option-item input[type="checkbox"]:checked {
accent-color: var(--primary);
}
.option-item.error {
border-color: var(--error);
background: rgba(220, 53, 69, 0.05);
}
/* Ensures radio/checkbox controls themselves use the primary color */
.option-item input[type="radio"]:checked,
.option-item input[type="radio"]:checked,
.option-item input[type="checkbox"]:checked {
accent-color: var(--primary);
}
@ -423,7 +455,7 @@
padding: 0 20px 20px;
}
}
/* === FIX FOR SMALL-SCREEN HAMBURGER MENU === */
@media (max-width: 991.98px) {
/* Add vertical spacing to the navigation items when the navbar is collapsed */
@ -431,15 +463,15 @@
margin-top: 5px;
margin-bottom: 5px;
padding: 5px 0;
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
#navbarNav .nav-item:last-child {
border-bottom: none;
border-bottom: none;
}
#navbarNav .nav-link {
padding: 8px 15px;
padding: 8px 15px;
display: block;
}
@ -451,13 +483,13 @@
#bottomNavbar {
/* Position the dark navbar 72px from the top of the viewport */
top: 72px;
top: 72px;
/* The z-index is already 1030 in the inline style, which is correct */
}
</style>
</head>
<body>
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1030;">
<div class="container-fluid">
<a class="navbar-brand text-white fw-bold" href="/">
@ -469,7 +501,7 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link text-secondary" href="/applications/">{% translate "Applications" %}</a>
</li>
@ -486,7 +518,7 @@
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
<span class="ms-2 text-white">JOB ID:&nbsp;&nbsp;{{job_id}}</span>
</nav>
<div class="page-content-wrapper">
<div class="wizard-container">
<div class="progress-container">
@ -529,10 +561,10 @@
// Placeholder for the complete JavaScript logic (omitted for brevity, but required for functionality)
// This script block should contain the Application State, DOM Elements, Validation, API, Rendering, and Event Handlers
console.log("JavaScript logic for form wizard needs to be included here.");
// --- COMPLETE JAVASCRIPT LOGIC GOES HERE ---
// (The logic provided in the previous turn, including the completed createFieldElement and renderPreview functions)
// Example structure for reference:
// function validateEmail(email) { ... }
// function loadFormTemplate() { ... }
@ -801,8 +833,8 @@
const formData = new FormData();
// Add applicant info
formData.append('applicant_name', state.formData.applicant_name || '');
formData.append('applicant_email', state.formData.applicant_email || '');
//formData.append('applicant_name', state.formData.applicant_name || '');
//formData.append('applicant_email', state.formData.applicant_email || '');
// Add field responses
state.stages.forEach(stage => {

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>
</div>
<div class="col-md-6">
{% if break_start_time and break_end_time %}
<p><strong>Break Time:</strong> {{ break_start_time|time:"g:i A" }} to {{ break_end_time|time:"g:i A" }}</p>
{% if breaks %}
<p><strong>Break Times:</strong></p>
<ul>
{% for break in breaks %}
<li>{{ break.start_time|time:"g:i A" }} to {{ break.end_time|time:"g:i A" }}</li>
{% endfor %}
</ul>
{% endif %}
<p><strong>Interview Duration:</strong> {{ interview_duration }} minutes</p>
<p><strong>Buffer Time:</strong> {{ buffer_time }} minutes</p>
@ -75,7 +80,7 @@
<button type="submit" name="confirm_schedule" class="btn btn-success">
<i class="fas fa-check"></i> Confirm Schedule
</button>
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary">
<a href="{% url 'schedule_interviews' job_id=job.id %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Edit
</a>
</form>
@ -109,13 +114,24 @@ document.addEventListener('DOMContentLoaded', function() {
}
},
{% endfor %}
{% for break in breaks %}
{
title: 'Break',
start: '{{ start_date|date:"Y-m-d" }}T{{ break.start_time|time:"H:i:s" }}',
end: '{{ start_date|date:"Y-m-d" }}T{{ break.end_time|time:"H:i:s" }}',
color: '#ff9f89',
display: 'background'
},
{% endfor %}
],
eventClick: function(info) {
// Show candidate details in a modal or alert
alert('Candidate: ' + info.event.title +
'\nDate: ' + info.event.start.toLocaleDateString() +
'\nTime: ' + info.event.extendedProps.time +
'\nEmail: ' + info.event.extendedProps.email);
if (info.event.title !== 'Break') {
alert('Candidate: ' + info.event.title +
'\nDate: ' + info.event.start.toLocaleDateString() +
'\nTime: ' + info.event.extendedProps.time +
'\nEmail: ' + info.event.extendedProps.email);
}
info.jsEvent.preventDefault();
}
});

View File

@ -7,7 +7,7 @@
<div class="card mt-4">
<div class="card-body">
<form method="post">
<form method="post" id="schedule-form">
{% csrf_token %}
<div class="row">
@ -21,31 +21,31 @@
<div class="col-md-6">
<h5>Schedule Details</h5>
<div class="form-group">
<div class="form-group mb-3">
<label for="{{ form.start_date.id_for_label }}">Start Date</label>
{{ form.start_date }}
</div>
<div class="form-group">
<div class="form-group mb-3">
<label for="{{ form.end_date.id_for_label }}">End Date</label>
{{ form.end_date }}
</div>
<div class="form-group">
<div class="form-group mb-3">
<label>Working Days</label>
{{ form.working_days }}
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="form-group mb-3">
<label for="{{ form.start_time.id_for_label }}">Start Time</label>
{{ form.start_time }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="form-group mb-3">
<label for="{{ form.end_time.id_for_label }}">End Time</label>
{{ form.end_time }}
</div>
@ -54,30 +54,14 @@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="{{ form.break_start_time.id_for_label }}">Break Start Time</label>
{{ form.break_start_time }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="{{ form.break_end_time.id_for_label }}">Break End Time</label>
{{ form.break_end_time }}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="form-group mb-3">
<label for="{{ form.interview_duration.id_for_label }}">Interview Duration (minutes)</label>
{{ form.interview_duration }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="form-group mb-3">
<label for="{{ form.buffer_time.id_for_label }}">Buffer Time (minutes)</label>
{{ form.buffer_time }}
</div>
@ -86,12 +70,85 @@
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<h5>Break Times</h5>
<div id="break-times-container">
{{ break_formset.management_form }}
{% for form in break_formset %}
<div class="break-time-form row mb-2">
<div class="col-md-5">
<label>Start Time</label>
{{ form.start_time }}
</div>
<div class="col-md-5">
<label>End Time</label>
{{ form.end_time }}
</div>
<div class="col-md-2">
<label>&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">
<button type="submit" class="btn btn-primary">Schedule Interviews</button>
<a href="{% url 'job_detail' slug=job.slug %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Preview Schedule</button>
<a href="{% url 'job_detail' pk=job.id %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const addBreakBtn = document.getElementById('add-break');
const breakTimesContainer = document.getElementById('break-times-container');
const totalFormsInput = document.getElementById('id_breaks-TOTAL_FORMS');
addBreakBtn.addEventListener('click', function() {
const formCount = parseInt(totalFormsInput.value);
const newFormHtml = `
<div class="break-time-form row mb-2">
<div class="col-md-5">
<label>Start Time</label>
<input type="time" name="breaks-${formCount}-start_time" class="form-control" id="id_breaks-${formCount}-start_time">
</div>
<div class="col-md-5">
<label>End Time</label>
<input type="time" name="breaks-${formCount}-end_time" class="form-control" id="id_breaks-${formCount}-end_time">
</div>
<div class="col-md-2">
<label>&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 %}

View File

@ -26,7 +26,7 @@
transform: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.card-header {
font-weight: 600;
padding: 1.25rem;
@ -52,7 +52,7 @@
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Action Buttons (used in table) */
.btn-group .btn-outline-primary {
color: var(--kaauh-teal);
@ -64,7 +64,7 @@
color: white;
border-color: var(--kaauh-teal);
}
/* Table Styling */
.table thead th {
color: var(--kaauh-teal-dark);
@ -78,19 +78,19 @@
.table tbody tr:hover {
background-color: #f3f9f9; /* Light teal hover for rows */
}
.table tbody td {
vertical-align: middle;
padding: 0.75rem 1rem;
}
/* Badge Styling */
.badge {
font-weight: 600;
padding: 0.4em 0.7em;
border-radius: 0.3rem;
}
/* Status Badge Mapping */
.table .bg-primary { background-color: var(--kaauh-teal) !important; color: white !important;} /* Job Title Badge */
.table .bg-success { background-color: #28a745 !important; } /* Applied: Yes */
@ -151,7 +151,7 @@
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
<th scope="col" style="width: 15%;">{% trans "Phone" %}</th>
<th scope="col" style="width: 15%;">{% trans "Job" %}</th>
<th scope="col" style="width: 10%;">{% trans "Applied" %}</th>
<th scope="col" style="width: 10%;">{% trans "Stage" %}</th>
<th scope="col" style="width: 10%;">{% trans "Created" %}</th>
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
</tr>
@ -164,8 +164,8 @@
<td>{{ candidate.phone }}</td>
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
<td>
<span class="badge {% if candidate.applied %}bg-success{% else %}bg-warning{% endif %}">
{{ candidate.applied|yesno:"Yes,No" }}
<span class="badge bg-primary">
{{ candidate.stage }}
</span>
</td>
<td>{{ candidate.created_at|date:"M d, Y" }}</td>