static file
This commit is contained in:
parent
a8a45195c1
commit
9b7c633a18
Binary file not shown.
@ -191,15 +191,6 @@ SOCIALACCOUNT_PROVIDERS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UNFOLD = {
|
|
||||||
"DASHBOARD_CALLBACK": "recruitment.utils.dashboard_callback",
|
|
||||||
"STYLES": [
|
|
||||||
lambda request: static("unfold/css/styles.css"),
|
|
||||||
],
|
|
||||||
"SCRIPTS": [
|
|
||||||
lambda request: static("unfold/js/app.js"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
||||||
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
||||||
|
|||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,7 +4,7 @@ from crispy_forms.helper import FormHelper
|
|||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from crispy_forms.layout import Layout, Submit, HTML, Div, Field
|
from crispy_forms.layout import Layout, Submit, HTML, Div, Field
|
||||||
from .models import ZoomMeeting, Candidate,Job,TrainingMaterial,JobPosting
|
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting
|
||||||
|
|
||||||
class CandidateForm(forms.ModelForm):
|
class CandidateForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
14
recruitment/migrations/0019_merge_20251006_1224.py
Normal file
14
recruitment/migrations/0019_merge_20251006_1224.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 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 = [
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
@ -16,22 +16,22 @@ class Base(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
# Create your models here.
|
# # Create your models here.
|
||||||
class Job(Base):
|
# class Job(Base):
|
||||||
title = models.CharField(max_length=255, verbose_name=_('Title'))
|
# title = models.CharField(max_length=255, verbose_name=_('Title'))
|
||||||
description_en = models.TextField(verbose_name=_('Description English'))
|
# description_en = models.TextField(verbose_name=_('Description English'))
|
||||||
description_ar = models.TextField(verbose_name=_('Description Arabic'))
|
# description_ar = models.TextField(verbose_name=_('Description Arabic'))
|
||||||
is_published = models.BooleanField(default=False, verbose_name=_('Published'))
|
# is_published = models.BooleanField(default=False, verbose_name=_('Published'))
|
||||||
posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn'))
|
# posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
|
# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at'))
|
# updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at'))
|
||||||
|
|
||||||
class Meta:
|
# class Meta:
|
||||||
verbose_name = _('Job')
|
# verbose_name = _('Job')
|
||||||
verbose_name_plural = _('Jobs')
|
# verbose_name_plural = _('Jobs')
|
||||||
|
|
||||||
def __str__(self):
|
# def __str__(self):
|
||||||
return self.title
|
# return self.title
|
||||||
|
|
||||||
class JobPosting(Base):
|
class JobPosting(Base):
|
||||||
# Basic Job Information
|
# Basic Job Information
|
||||||
@ -506,4 +506,43 @@ class SharedFormTemplate(models.Model):
|
|||||||
verbose_name_plural = 'Shared Form Templates'
|
verbose_name_plural = 'Shared Form Templates'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Shared: {self.template.name}"
|
return f"Shared: {self.template.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Source(models.Model):
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True,
|
||||||
|
verbose_name=_('Source Name'),
|
||||||
|
help_text=_("e.g., ATS, ERP ")
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Source')
|
||||||
|
verbose_name_plural = _('Sources')
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
class HiringAgency(Base):
|
||||||
|
name = models.CharField(max_length=200, unique=True, verbose_name=_('Agency Name'))
|
||||||
|
contact_person = models.CharField(max_length=150, blank=True, verbose_name=_('Contact Person'))
|
||||||
|
email = models.EmailField(blank=True)
|
||||||
|
phone = models.CharField(max_length=20, blank=True)
|
||||||
|
website = models.URLField(blank=True)
|
||||||
|
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
|
||||||
|
country=CountryField(blank=True, null=True,blank_label=_('Select country'))
|
||||||
|
address=models.TextField(blank=True,null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Hiring Agency')
|
||||||
|
verbose_name_plural = _('Hiring Agencies')
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
recruitment/templatetags/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
recruitment/templatetags/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -14,7 +14,7 @@ from django.contrib import messages
|
|||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from .linkedin_service import LinkedInService
|
from .linkedin_service import LinkedInService
|
||||||
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission
|
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission
|
||||||
from .models import ZoomMeeting, Job, Candidate, JobPosting
|
from .models import ZoomMeeting, Candidate, JobPosting
|
||||||
from .serializers import JobPostingSerializer, CandidateSerializer
|
from .serializers import JobPostingSerializer, CandidateSerializer
|
||||||
from django.shortcuts import get_object_or_404, render, redirect
|
from django.shortcuts import get_object_or_404, render, redirect
|
||||||
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
||||||
|
|||||||
0
static/image/applicant/__init__.py
Normal file
0
static/image/applicant/__init__.py
Normal file
BIN
static/image/applicant/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/admin.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/apps.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/forms.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/forms_builder.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/forms_builder.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/models.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/urls.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
static/image/applicant/__pycache__/views.cpython-312.pyc
Normal file
BIN
static/image/applicant/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
static/image/applicant/admin.py
Normal file
3
static/image/applicant/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
static/image/applicant/apps.py
Normal file
6
static/image/applicant/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicantConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'applicant'
|
||||||
22
static/image/applicant/forms.py
Normal file
22
static/image/applicant/forms.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import ApplicantForm, FormField
|
||||||
|
|
||||||
|
class ApplicantFormCreateForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ApplicantForm
|
||||||
|
fields = ['name', 'description']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormFieldForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = FormField
|
||||||
|
fields = ['label', 'field_type', 'required', 'help_text', 'choices']
|
||||||
|
widgets = {
|
||||||
|
'label': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'field_type': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'help_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||||
|
'choices': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Option1, Option2, Option3'}),
|
||||||
|
}
|
||||||
49
static/image/applicant/forms_builder.py
Normal file
49
static/image/applicant/forms_builder.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import FormField
|
||||||
|
|
||||||
|
# applicant/forms_builder.py
|
||||||
|
def create_dynamic_form(form_instance):
|
||||||
|
fields = {}
|
||||||
|
|
||||||
|
for field in form_instance.fields.all():
|
||||||
|
field_kwargs = {
|
||||||
|
'label': field.label,
|
||||||
|
'required': field.required,
|
||||||
|
'help_text': field.help_text
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use stable field_name instead of database ID
|
||||||
|
field_key = field.field_name
|
||||||
|
|
||||||
|
if field.field_type == 'text':
|
||||||
|
fields[field_key] = forms.CharField(**field_kwargs)
|
||||||
|
elif field.field_type == 'email':
|
||||||
|
fields[field_key] = forms.EmailField(**field_kwargs)
|
||||||
|
elif field.field_type == 'phone':
|
||||||
|
fields[field_key] = forms.CharField(**field_kwargs)
|
||||||
|
elif field.field_type == 'number':
|
||||||
|
fields[field_key] = forms.IntegerField(**field_kwargs)
|
||||||
|
elif field.field_type == 'date':
|
||||||
|
fields[field_key] = forms.DateField(**field_kwargs)
|
||||||
|
elif field.field_type == 'textarea':
|
||||||
|
fields[field_key] = forms.CharField(
|
||||||
|
widget=forms.Textarea,
|
||||||
|
**field_kwargs
|
||||||
|
)
|
||||||
|
elif field.field_type in ['select', 'radio']:
|
||||||
|
choices = [(c.strip(), c.strip()) for c in field.choices.split(',') if c.strip()]
|
||||||
|
if not choices:
|
||||||
|
choices = [('', '---')]
|
||||||
|
if field.field_type == 'select':
|
||||||
|
fields[field_key] = forms.ChoiceField(choices=choices, **field_kwargs)
|
||||||
|
else:
|
||||||
|
fields[field_key] = forms.ChoiceField(
|
||||||
|
choices=choices,
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
**field_kwargs
|
||||||
|
)
|
||||||
|
elif field.field_type == 'checkbox':
|
||||||
|
field_kwargs['required'] = False
|
||||||
|
fields[field_key] = forms.BooleanField(**field_kwargs)
|
||||||
|
|
||||||
|
return type('DynamicApplicantForm', (forms.Form,), fields)
|
||||||
70
static/image/applicant/migrations/0001_initial.py
Normal file
70
static/image/applicant/migrations/0001_initial.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-01 21:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jobs', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ApplicantForm',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text="Form version name (e.g., 'Version A', 'Version B' etc)", max_length=200)),
|
||||||
|
('description', models.TextField(blank=True, help_text='Optional description of this form version')),
|
||||||
|
('is_active', models.BooleanField(default=False, help_text='Only one form can be active per job')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applicant_forms', to='jobs.jobposting')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Application Form',
|
||||||
|
'verbose_name_plural': 'Application Forms',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('job_posting', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ApplicantSubmission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('submitted_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('data', models.JSONField()),
|
||||||
|
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||||
|
('score', models.FloatField(default=0, help_text='Ranking score for the applicant submission')),
|
||||||
|
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applicant.applicantform')),
|
||||||
|
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='jobs.jobposting')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Applicant Submission',
|
||||||
|
'verbose_name_plural': 'Applicant Submissions',
|
||||||
|
'ordering': ['-submitted_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FormField',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('label', models.CharField(max_length=255)),
|
||||||
|
('field_type', models.CharField(choices=[('text', 'Text'), ('email', 'Email'), ('phone', 'Phone'), ('number', 'Number'), ('date', 'Date'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkbox'), ('textarea', 'Paragraph Text'), ('file', 'File Upload'), ('image', 'Image Upload')], max_length=20)),
|
||||||
|
('required', models.BooleanField(default=True)),
|
||||||
|
('help_text', models.TextField(blank=True)),
|
||||||
|
('choices', models.TextField(blank=True, help_text='Comma-separated options for select/radio fields')),
|
||||||
|
('order', models.IntegerField(default=0)),
|
||||||
|
('field_name', models.CharField(blank=True, max_length=100)),
|
||||||
|
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='applicant.applicantform')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Form Field',
|
||||||
|
'verbose_name_plural': 'Form Fields',
|
||||||
|
'ordering': ['order'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
static/image/applicant/migrations/__init__.py
Normal file
0
static/image/applicant/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
144
static/image/applicant/models.py
Normal file
144
static/image/applicant/models.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# models.py
|
||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from jobs.models import JobPosting
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
class ApplicantForm(models.Model):
|
||||||
|
"""Multiple dynamic forms per job posting, only one active at a time"""
|
||||||
|
job_posting = models.ForeignKey(
|
||||||
|
JobPosting,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='applicant_forms'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
help_text="Form version name (e.g., 'Version A', 'Version B' etc)"
|
||||||
|
)
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Optional description of this form version"
|
||||||
|
)
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Only one form can be active per job"
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('job_posting', 'name')
|
||||||
|
ordering = ['-created_at']
|
||||||
|
verbose_name = "Application Form"
|
||||||
|
verbose_name_plural = "Application Forms"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
status = "(Active)" if self.is_active else "(Inactive)"
|
||||||
|
return f"{self.name} for {self.job_posting.title} {status}"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Ensure only one active form per job"""
|
||||||
|
if self.is_active:
|
||||||
|
existing_active = self.job_posting.applicant_forms.filter(
|
||||||
|
is_active=True
|
||||||
|
).exclude(pk=self.pk)
|
||||||
|
if existing_active.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
"Only one active application form is allowed per job posting."
|
||||||
|
)
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
"""Set this form as active and deactivate others"""
|
||||||
|
self.is_active = True
|
||||||
|
self.save()
|
||||||
|
# Deactivate other forms
|
||||||
|
self.job_posting.applicant_forms.exclude(pk=self.pk).update(
|
||||||
|
is_active=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_public_url(self):
|
||||||
|
"""Returns the public application URL for this job's active form"""
|
||||||
|
return reverse('applicant:apply_form', args=[self.job_posting.internal_job_id])
|
||||||
|
|
||||||
|
|
||||||
|
class FormField(models.Model):
|
||||||
|
FIELD_TYPES = [
|
||||||
|
('text', 'Text'),
|
||||||
|
('email', 'Email'),
|
||||||
|
('phone', 'Phone'),
|
||||||
|
('number', 'Number'),
|
||||||
|
('date', 'Date'),
|
||||||
|
('select', 'Dropdown'),
|
||||||
|
('radio', 'Radio Buttons'),
|
||||||
|
('checkbox', 'Checkbox'),
|
||||||
|
('textarea', 'Paragraph Text'),
|
||||||
|
('file', 'File Upload'),
|
||||||
|
('image', 'Image Upload'),
|
||||||
|
]
|
||||||
|
|
||||||
|
form = models.ForeignKey(
|
||||||
|
ApplicantForm,
|
||||||
|
related_name='fields',
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
label = models.CharField(max_length=255)
|
||||||
|
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
|
||||||
|
required = models.BooleanField(default=True)
|
||||||
|
help_text = models.TextField(blank=True)
|
||||||
|
choices = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Comma-separated options for select/radio fields"
|
||||||
|
)
|
||||||
|
order = models.IntegerField(default=0)
|
||||||
|
field_name = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order']
|
||||||
|
verbose_name = "Form Field"
|
||||||
|
verbose_name_plural = "Form Fields"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.label} ({self.field_type}) in {self.form.name}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.field_name:
|
||||||
|
# Create a stable field name from label (e.g., "Full Name" → "full_name")
|
||||||
|
import re
|
||||||
|
# Use Unicode word characters, including Arabic, for field_name
|
||||||
|
self.field_name = re.sub(
|
||||||
|
r'[^\w]+',
|
||||||
|
'_',
|
||||||
|
self.label.lower(),
|
||||||
|
flags=re.UNICODE
|
||||||
|
).strip('_')
|
||||||
|
# Ensure uniqueness within the form
|
||||||
|
base_name = self.field_name
|
||||||
|
counter = 1
|
||||||
|
while FormField.objects.filter(
|
||||||
|
form=self.form,
|
||||||
|
field_name=self.field_name
|
||||||
|
).exists():
|
||||||
|
self.field_name = f"{base_name}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicantSubmission(models.Model):
|
||||||
|
job_posting = models.ForeignKey(JobPosting, on_delete=models.CASCADE)
|
||||||
|
form = models.ForeignKey(ApplicantForm, on_delete=models.CASCADE)
|
||||||
|
submitted_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
data = models.JSONField()
|
||||||
|
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||||
|
score = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
help_text="Ranking score for the applicant submission"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-submitted_at']
|
||||||
|
verbose_name = "Applicant Submission"
|
||||||
|
verbose_name_plural = "Applicant Submissions"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Submission for {self.job_posting.title} at {self.submitted_at}"
|
||||||
94
static/image/applicant/templates/applicant/apply_form.html
Normal file
94
static/image/applicant/templates/applicant/apply_form.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Apply: {{ job.title }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
{# --- 1. Job Header and Overview (Fixed/Static Info) --- #}
|
||||||
|
<div class="card bg-light-subtle mb-4 p-4 border-0 rounded-3 shadow-sm">
|
||||||
|
<h1 class="h2 fw-bold text-primary mb-1">{{ job.title }}</h1>
|
||||||
|
|
||||||
|
<p class="mb-3 text-muted">
|
||||||
|
Your final step to apply for this position.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-flex gap-4 small text-secondary">
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-building me-1"></i>
|
||||||
|
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-map-marker-alt me-1"></i>
|
||||||
|
<strong>Location:</strong> {{ job.get_location_display }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-briefcase me-1"></i>
|
||||||
|
<strong>Type:</strong> {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# --- 2. Application Form Section --- #}
|
||||||
|
<div class="card p-5 border-0 rounded-3 shadow">
|
||||||
|
<h2 class="h3 fw-semibold mb-3">Application Details</h2>
|
||||||
|
|
||||||
|
{% if applicant_form.description %}
|
||||||
|
<p class="text-muted mb-4">{{ applicant_form.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
{# Label Tag #}
|
||||||
|
<label for="{{ field.id_for_label }}" class="form-label">
|
||||||
|
{{ field.label }}
|
||||||
|
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{# The Field Widget (Assumes form-control is applied in backend) #}
|
||||||
|
{{ field }}
|
||||||
|
|
||||||
|
{# Field Errors #}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ field.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Help Text #}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{# General Form Errors (Non-field errors) #}
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger mb-4">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg mt-3 w-100">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i> Submit Application
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-4 text-center">
|
||||||
|
<a href="{% url 'applicant:review_job_detail' job.internal_job_id %}"
|
||||||
|
class="btn btn-link text-secondary">
|
||||||
|
← Review Job Details
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
68
static/image/applicant/templates/applicant/create_form.html
Normal file
68
static/image/applicant/templates/applicant/create_form.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Define Form for {{ job.title }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8 col-md-10">
|
||||||
|
|
||||||
|
<div class="card shadow-lg border-0 p-4 p-md-5">
|
||||||
|
|
||||||
|
<h2 class="card-title text-center mb-4 text-dark">
|
||||||
|
🛠️ New Application Form Configuration
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mb-4 border-bottom pb-3">
|
||||||
|
You are creating a new form structure for job: <strong>{{ job.title }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<fieldset class="mb-5">
|
||||||
|
<legend class="h5 mb-3 text-secondary">Form Metadata</legend>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="{{ form.name.id_for_label }}" class="form-label required">
|
||||||
|
Form Name
|
||||||
|
</label>
|
||||||
|
{# The field should already have form-control applied from the backend #}
|
||||||
|
{{ form.name }}
|
||||||
|
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
{# The field should already have form-control applied from the backend #}
|
||||||
|
{{ form.description}}
|
||||||
|
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.description.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-3 pt-3">
|
||||||
|
<a href="{% url 'applicant:job_forms_list' job.internal_job_id %}"
|
||||||
|
class="btn btn-outline-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn univ-color btn-lg">
|
||||||
|
Create Form & Continue →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
1020
static/image/applicant/templates/applicant/edit_form.html
Normal file
1020
static/image/applicant/templates/applicant/edit_form.html
Normal file
File diff suppressed because it is too large
Load Diff
103
static/image/applicant/templates/applicant/job_forms_list.html
Normal file
103
static/image/applicant/templates/applicant/job_forms_list.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Manage Forms | {{ job.title }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10">
|
||||||
|
|
||||||
|
<header class="mb-5 pb-3 border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="h3 mb-1 ">
|
||||||
|
<i class="fas fa-clipboard-list me-2 text-secondary"></i>
|
||||||
|
Application Forms for <span class="text-success fw-bold">"{{ job.title }}"</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted small">
|
||||||
|
Internal Job ID: **{{ job.internal_job_id }}**
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Primary Action Button using the theme color #}
|
||||||
|
<a href="{% url 'applicant:create_form' job_id=job.internal_job_id %}"
|
||||||
|
class="btn univ-color btn-lg shadow-sm mt-3 mt-md-0">
|
||||||
|
<i class="fas fa-plus me-1"></i> Create New Form
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if forms %}
|
||||||
|
|
||||||
|
<div class="list-group">
|
||||||
|
{% for form in forms %}
|
||||||
|
|
||||||
|
{# Custom styling based on active state #}
|
||||||
|
<div class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center p-3 mb-3 rounded shadow-sm
|
||||||
|
{% if form.is_active %}border-success border-3 bg-light{% else %}border-secondary border-1{% endif %}">
|
||||||
|
|
||||||
|
{# Left Section: Form Details #}
|
||||||
|
<div class="flex-grow-1 me-4 mb-2 mb-sm-0">
|
||||||
|
<h4 class="h5 mb-1 d-inline-block">
|
||||||
|
{{ form.name }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{# Status Badge #}
|
||||||
|
{% if form.is_active %}
|
||||||
|
<span class="badge bg-success ms-2">
|
||||||
|
<i class="fas fa-check-circle me-1"></i> Active Form
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary ms-2">
|
||||||
|
<i class="fas fa-times-circle me-1"></i> Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="text-muted mt-1 mb-1 small">
|
||||||
|
{{ form.description|default:"— No description provided. —" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Right Section: Actions #}
|
||||||
|
<div class="d-flex gap-2 align-items-center flex-wrap">
|
||||||
|
|
||||||
|
{# Edit Structure Button #}
|
||||||
|
<a href="{% url 'applicant:edit_form' form.id %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-pen me-1"></i> Edit Structure
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{# Conditional Activation Button #}
|
||||||
|
{% if not form.is_active %}
|
||||||
|
<a href="{% url 'applicant:activate_form' form.id %}"
|
||||||
|
class="btn btn-sm univ-color">
|
||||||
|
<i class="fas fa-bolt me-1"></i> Activate Form
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{# Active indicator/Deactivate button placeholder #}
|
||||||
|
<a href="#" class="btn btn-sm btn-outline-success" disabled>
|
||||||
|
<i class="fas fa-star me-1"></i> Current Form
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5 bg-light rounded shadow-sm">
|
||||||
|
<i class="fas fa-file-alt fa-4x text-muted mb-3"></i>
|
||||||
|
<p class="lead mb-0">No application forms have been created yet for this job.</p>
|
||||||
|
<p class="mt-2 mb-0 text-secondary">Click the button above to define a new form structure.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<footer class="text-end mt-5 pt-3 border-top">
|
||||||
|
<a href="{% url 'jobs:job_detail' job.internal_job_id %}"
|
||||||
|
class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Back to Job Details
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h2>{{ job.title }}</h2>
|
||||||
|
<span class="badge bg-{{ job.status|lower }} status-badge">
|
||||||
|
{{ job.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Job Details -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Position Number:</strong> {{ job.position_number|default:"Not specified" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Job Type:</strong> {{ job.get_job_type_display }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Workplace:</strong> {{ job.get_workplace_type_display }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Location:</strong> {{ job.get_location_display }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Created By:</strong> {{ job.created_by|default:"Not specified" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if job.salary_range %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Salary Range:</strong> {{ job.salary_range }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if job.start_date %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Start Date:</strong> {{ job.start_date }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if job.application_deadline %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Application Deadline:</strong> {{ job.application_deadline }}
|
||||||
|
{% if job.is_expired %}
|
||||||
|
<span class="badge bg-danger">EXPIRED</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{% if job.description %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5>Description</h5>
|
||||||
|
<div>{{ job.description|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if job.qualifications %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5>Qualifications</h5>
|
||||||
|
<div>{{ job.qualifications|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if job.benefits %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5>Benefits</h5>
|
||||||
|
<div>{{ job.benefits|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if job.application_instructions %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5>Application Instructions</h5>
|
||||||
|
<div>{{ job.application_instructions|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
|
||||||
|
<!-- Add this section below your existing job details -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5><i class="fas fa-file-signature"></i> Ready to Apply?</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Review the job details on the left, then click the button below to submit your application.</p>
|
||||||
|
<a href="{% url 'applicant:apply_form' job.internal_job_id %}" class="btn btn-success btn-lg w-100">
|
||||||
|
<i class="fas fa-paper-plane"></i> Apply for this Position
|
||||||
|
</a>
|
||||||
|
<p class="text-muted mt-2">
|
||||||
|
<small>You'll be redirected to our secure application form where you can upload your resume and provide additional details.</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
35
static/image/applicant/templates/applicant/thank_you.html
Normal file
35
static/image/applicant/templates/applicant/thank_you.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Application Submitted - {{ job.title }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div style="text-align: center; padding: 30px 0;">
|
||||||
|
<div style="width: 80px; height: 80px; background: #d4edda; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 20px;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="#28a745" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color: #28a745; margin-bottom: 15px;">Thank You!</h1>
|
||||||
|
<h2>Your application has been submitted successfully</h2>
|
||||||
|
|
||||||
|
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0; text-align: left;">
|
||||||
|
<p><strong>Position:</strong> {{ job.title }}</p>
|
||||||
|
<p><strong>Job ID:</strong> {{ job.internal_job_id }}</p>
|
||||||
|
<p><strong>Department:</strong> {{ job.department|default:"Not specified" }}</p>
|
||||||
|
{% if job.application_deadline %}
|
||||||
|
<p><strong>Application Deadline:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size: 18px; line-height: 1.6;">
|
||||||
|
We appreciate your interest in joining our team. Our hiring team will review your application
|
||||||
|
and contact you if there's a potential match for this position.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% comment %} <div style="margin-top: 30px;">
|
||||||
|
<a href="/" class="btn btn-primary" style="margin-right: 10px;">Apply to Another Position</a>
|
||||||
|
<a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-outline">View Job Details</a>
|
||||||
|
</div> {% endcomment %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
0
static/image/applicant/templatetags/__init__.py
Normal file
0
static/image/applicant/templatetags/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
24
static/image/applicant/templatetags/mytags.py
Normal file
24
static/image/applicant/templatetags/mytags.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import json
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter(name='from_json')
|
||||||
|
def from_json(json_string):
|
||||||
|
"""
|
||||||
|
Safely loads a JSON string into a Python object (list or dict).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# The JSON string comes from the context and needs to be parsed
|
||||||
|
return json.loads(json_string)
|
||||||
|
except (TypeError, json.JSONDecodeError):
|
||||||
|
# Handle cases where the string is invalid or None/empty
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='split')
|
||||||
|
def split_string(value, key=None):
|
||||||
|
"""Splits a string by the given key (default is space)."""
|
||||||
|
if key is None:
|
||||||
|
return value.split()
|
||||||
|
return value.split(key)
|
||||||
14
static/image/applicant/templatetags/signals.py
Normal file
14
static/image/applicant/templatetags/signals.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# from django.db.models.signals import post_save
|
||||||
|
# from django.dispatch import receiver
|
||||||
|
# from . import models
|
||||||
|
#
|
||||||
|
# @receiver(post_save, sender=models.Candidate)
|
||||||
|
# def parse_resume(sender, instance, created, **kwargs):
|
||||||
|
# if instance.resume and not instance.summary:
|
||||||
|
# from .utils import extract_summary_from_pdf,match_resume_with_job_description
|
||||||
|
# summary = extract_summary_from_pdf(instance.resume.path)
|
||||||
|
# if 'error' not in summary:
|
||||||
|
# instance.summary = summary
|
||||||
|
# instance.save()
|
||||||
|
#
|
||||||
|
# # match_resume_with_job_description
|
||||||
3
static/image/applicant/tests.py
Normal file
3
static/image/applicant/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
18
static/image/applicant/urls.py
Normal file
18
static/image/applicant/urls.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'applicant'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Form Management
|
||||||
|
path('job/<str:job_id>/forms/', views.job_forms_list, name='job_forms_list'),
|
||||||
|
path('job/<str:job_id>/forms/create/', views.create_form_for_job, name='create_form'),
|
||||||
|
path('form/<int:form_id>/edit/', views.edit_form, name='edit_form'),
|
||||||
|
path('field/<int:field_id>/delete/', views.delete_field, name='delete_field'),
|
||||||
|
path('form/<int:form_id>/activate/', views.activate_form, name='activate_form'),
|
||||||
|
|
||||||
|
# Public Application
|
||||||
|
path('apply/<str:job_id>/', views.apply_form_view, name='apply_form'),
|
||||||
|
path('review/job/detail/<str:job_id>/',views.review_job_detail, name="review_job_detail"),
|
||||||
|
path('apply/<str:job_id>/thank-you/', views.thank_you_view, name='thank_you'),
|
||||||
|
]
|
||||||
34
static/image/applicant/utils.py
Normal file
34
static/image/applicant/utils.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import os
|
||||||
|
import fitz # PyMuPDF
|
||||||
|
import spacy
|
||||||
|
import requests
|
||||||
|
from recruitment import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
nlp = spacy.load("en_core_web_sm")
|
||||||
|
|
||||||
|
def extract_text_from_pdf(pdf_path):
|
||||||
|
text = ""
|
||||||
|
with fitz.open(pdf_path) as doc:
|
||||||
|
for page in doc:
|
||||||
|
text += page.get_text()
|
||||||
|
return text
|
||||||
|
|
||||||
|
def extract_summary_from_pdf(pdf_path):
|
||||||
|
if not os.path.exists(pdf_path):
|
||||||
|
return {'error': 'File not found'}
|
||||||
|
|
||||||
|
text = extract_text_from_pdf(pdf_path)
|
||||||
|
doc = nlp(text)
|
||||||
|
summary = {
|
||||||
|
'name': doc.ents[0].text if doc.ents else '',
|
||||||
|
'skills': [chunk.text for chunk in doc.noun_chunks if len(chunk.text.split()) > 1],
|
||||||
|
'summary': text[:500]
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
|
||||||
|
def match_resume_with_job_description(resume, job_description,prompt=""):
|
||||||
|
resume_doc = nlp(resume)
|
||||||
|
job_doc = nlp(job_description)
|
||||||
|
similarity = resume_doc.similarity(job_doc)
|
||||||
|
return similarity
|
||||||
175
static/image/applicant/views.py
Normal file
175
static/image/applicant/views.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# applicant/views.py (Updated edit_form function)
|
||||||
|
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import Http404, JsonResponse # <-- Import JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt # <-- Needed for JSON POST if not using FormData
|
||||||
|
import json # <-- Import json
|
||||||
|
from django.db import transaction # <-- Import transaction
|
||||||
|
|
||||||
|
# (Keep all your existing imports)
|
||||||
|
from .models import ApplicantForm, FormField, ApplicantSubmission
|
||||||
|
from .forms import ApplicantFormCreateForm, FormFieldForm
|
||||||
|
from jobs.models import JobPosting
|
||||||
|
from .forms_builder import create_dynamic_form
|
||||||
|
|
||||||
|
# ... (Keep all other functions like job_forms_list, create_form_for_job, etc.)
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# === FORM MANAGEMENT VIEWS ===
|
||||||
|
|
||||||
|
def job_forms_list(request, job_id):
|
||||||
|
"""List all forms for a specific job"""
|
||||||
|
job = get_object_or_404(JobPosting, internal_job_id=job_id)
|
||||||
|
forms = job.applicant_forms.all()
|
||||||
|
return render(request, 'applicant/job_forms_list.html', {
|
||||||
|
'job': job,
|
||||||
|
'forms': forms
|
||||||
|
})
|
||||||
|
|
||||||
|
def create_form_for_job(request, job_id):
|
||||||
|
"""Create a new form for a job"""
|
||||||
|
job = get_object_or_404(JobPosting, internal_job_id=job_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ApplicantFormCreateForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
applicant_form = form.save(commit=False)
|
||||||
|
applicant_form.job_posting = job
|
||||||
|
applicant_form.save()
|
||||||
|
messages.success(request, 'Form created successfully!')
|
||||||
|
return redirect('applicant:job_forms_list', job_id=job_id)
|
||||||
|
else:
|
||||||
|
form = ApplicantFormCreateForm()
|
||||||
|
|
||||||
|
return render(request, 'applicant/create_form.html', {
|
||||||
|
'job': job,
|
||||||
|
'form': form
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic # Ensures all fields are saved or none are
|
||||||
|
def edit_form(request, form_id):
|
||||||
|
"""Edit form details and manage fields, including dynamic builder save."""
|
||||||
|
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
|
||||||
|
job = applicant_form.job_posting
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# --- 1. Handle JSON data from the Form Builder (JavaScript) ---
|
||||||
|
if request.content_type == 'application/json':
|
||||||
|
try:
|
||||||
|
field_data = json.loads(request.body)
|
||||||
|
|
||||||
|
# Clear existing fields for this form
|
||||||
|
applicant_form.fields.all().delete()
|
||||||
|
|
||||||
|
# Create new fields from the JSON data
|
||||||
|
for field_config in field_data:
|
||||||
|
# Sanitize/ensure required fields are present
|
||||||
|
FormField.objects.create(
|
||||||
|
form=applicant_form,
|
||||||
|
label=field_config.get('label', 'New Field'),
|
||||||
|
field_type=field_config.get('field_type', 'text'),
|
||||||
|
required=field_config.get('required', True),
|
||||||
|
help_text=field_config.get('help_text', ''),
|
||||||
|
choices=field_config.get('choices', ''),
|
||||||
|
order=field_config.get('order', 0),
|
||||||
|
# field_name will be auto-generated/re-generated on save() if needed
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'success', 'message': 'Form structure saved successfully!'})
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({'status': 'error', 'message': 'Invalid JSON data.'}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'status': 'error', 'message': f'Server error: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
# --- 2. Handle standard POST requests (e.g., saving form details) ---
|
||||||
|
elif 'save_form_details' in request.POST: # Changed the button name for clarity
|
||||||
|
form_details = ApplicantFormCreateForm(request.POST, instance=applicant_form)
|
||||||
|
if form_details.is_valid():
|
||||||
|
form_details.save()
|
||||||
|
messages.success(request, 'Form details updated successfully!')
|
||||||
|
return redirect('applicant:edit_form', form_id=form_id)
|
||||||
|
|
||||||
|
# Note: The 'add_field' branch is now redundant since we use the builder,
|
||||||
|
# but you can keep it if you want the old manual way too.
|
||||||
|
|
||||||
|
# --- GET Request (or unsuccessful POST) ---
|
||||||
|
form_details = ApplicantFormCreateForm(instance=applicant_form)
|
||||||
|
# Get initial fields to load into the JS builder
|
||||||
|
initial_fields_json = list(applicant_form.fields.values(
|
||||||
|
'label', 'field_type', 'required', 'help_text', 'choices', 'order', 'field_name'
|
||||||
|
))
|
||||||
|
|
||||||
|
return render(request, 'applicant/edit_form.html', {
|
||||||
|
'applicant_form': applicant_form,
|
||||||
|
'job': job,
|
||||||
|
'form_details': form_details,
|
||||||
|
'initial_fields_json': json.dumps(initial_fields_json)
|
||||||
|
})
|
||||||
|
|
||||||
|
def delete_field(request, field_id):
|
||||||
|
"""Delete a form field"""
|
||||||
|
field = get_object_or_404(FormField, id=field_id)
|
||||||
|
form_id = field.form.id
|
||||||
|
field.delete()
|
||||||
|
messages.success(request, 'Field deleted successfully!')
|
||||||
|
return redirect('applicant:edit_form', form_id=form_id)
|
||||||
|
|
||||||
|
def activate_form(request, form_id):
|
||||||
|
"""Activate a form (deactivates others automatically)"""
|
||||||
|
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
|
||||||
|
applicant_form.activate()
|
||||||
|
messages.success(request, f'Form "{applicant_form.name}" is now active!')
|
||||||
|
return redirect('applicant:job_forms_list', job_id=applicant_form.job_posting.internal_job_id)
|
||||||
|
|
||||||
|
# === PUBLIC VIEWS (for applicants) ===
|
||||||
|
|
||||||
|
def apply_form_view(request, job_id):
|
||||||
|
"""Public application form - serves active form"""
|
||||||
|
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
|
||||||
|
|
||||||
|
if job.is_expired():
|
||||||
|
raise Http404("Application deadline has passed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
applicant_form = job.applicant_forms.get(is_active=True)
|
||||||
|
except ApplicantForm.DoesNotExist:
|
||||||
|
raise Http404("No active application form configured for this job")
|
||||||
|
|
||||||
|
DynamicForm = create_dynamic_form(applicant_form)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = DynamicForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
ApplicantSubmission.objects.create(
|
||||||
|
job_posting=job,
|
||||||
|
form=applicant_form,
|
||||||
|
data=form.cleaned_data,
|
||||||
|
ip_address=request.META.get('REMOTE_ADDR')
|
||||||
|
)
|
||||||
|
return redirect('applicant:thank_you', job_id=job_id)
|
||||||
|
else:
|
||||||
|
form = DynamicForm()
|
||||||
|
|
||||||
|
return render(request, 'applicant/apply_form.html', {
|
||||||
|
'form': form,
|
||||||
|
'job': job,
|
||||||
|
'applicant_form': applicant_form
|
||||||
|
})
|
||||||
|
|
||||||
|
def review_job_detail(request,job_id):
|
||||||
|
"""Public job detail view for applicants"""
|
||||||
|
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
|
||||||
|
if job.is_expired():
|
||||||
|
raise Http404("This job posting has expired.")
|
||||||
|
return render(request,'applicant/review_job_detail.html',{'job':job})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def thank_you_view(request, job_id):
|
||||||
|
job = get_object_or_404(JobPosting, internal_job_id=job_id)
|
||||||
|
return render(request, 'applicant/thank_you.html', {'job': job})
|
||||||
BIN
static/image/hospital_logo.png
Normal file
BIN
static/image/hospital_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
static/image/hospital_logo_1.png
Normal file
BIN
static/image/hospital_logo_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
BIN
static/image/hospital_logo_2.png
Normal file
BIN
static/image/hospital_logo_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
BIN
static/image/hospital_logo_3.png
Normal file
BIN
static/image/hospital_logo_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
8
static/image/vision.svg
Normal file
8
static/image/vision.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
BIN
static/media/form_uploads/jitendra.pdf
Normal file
BIN
static/media/form_uploads/jitendra.pdf
Normal file
Binary file not shown.
BIN
static/media/form_uploads/resume_juanjosecarin.pdf
Normal file
BIN
static/media/form_uploads/resume_juanjosecarin.pdf
Normal file
Binary file not shown.
@ -1,86 +0,0 @@
|
|||||||
{% extends 'unfold/layouts/base.html' %}
|
|
||||||
{% load i18n unfold %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{% trans 'Dashboard' %} | {{ site_title|default:_('Django site admin') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block branding %}
|
|
||||||
<h1 id="site-name">
|
|
||||||
<a href="{% url 'admin:index' %}">
|
|
||||||
{{ site_header }}
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content_before %}
|
|
||||||
{% component "unfold/helpers/header.html" %}
|
|
||||||
{% trans "Recruitment Dashboard" %}
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
||||||
{% component "unfold/components/card.html" with title="Total Jobs Posted" %}
|
|
||||||
<p class="text-4xl font-bold text-blue-600">{{ total_jobs }}</p>
|
|
||||||
{% endcomponent %}
|
|
||||||
|
|
||||||
{% component "unfold/components/card.html" with title="Total Candidates" %}
|
|
||||||
<p class="text-4xl font-bold text-green-600">{{ total_candidates }}</p>
|
|
||||||
{% endcomponent %}
|
|
||||||
|
|
||||||
{% component "unfold/components/card.html" with title="Average Applications/Job" %}
|
|
||||||
<p class="text-4xl font-bold text-yellow-600">{{ average_applications }}</p>
|
|
||||||
{% endcomponent %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white p-6 rounded-xl shadow-md mb-8">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-800">Applications Per Job</h2>
|
|
||||||
</div>
|
|
||||||
<canvas id="applicationsChart" height="300"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<script>
|
|
||||||
const ctx = document.getElementById('applicationsChart').getContext('2d');
|
|
||||||
const applicationsChart = new Chart(ctx, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: {{ job_titles|safe }},
|
|
||||||
datasets: [{
|
|
||||||
label: 'Applications',
|
|
||||||
data: {{ job_app_counts|safe }},
|
|
||||||
backgroundColor: 'rgba(34, 197, 94, 0.7)',
|
|
||||||
borderColor: 'rgba(34, 197, 94, 1)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 4,
|
|
||||||
maxBarThickness: 40
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
return `${context.raw} applications`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: { ticks: { autoSkip: false } },
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
grace: '10%',
|
|
||||||
title: { display: true, text: 'Number of Applicants' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{% load static i18n %}
|
{% comment %} {% load static i18n %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{% if request.LANGUAGE_CODE %}{{ request.LANGUAGE_CODE }}{% else %}en{% endif %}">
|
<html lang="{% if request.LANGUAGE_CODE %}{{ request.LANGUAGE_CODE }}{% else %}en{% endif %}">
|
||||||
<head>
|
<head>
|
||||||
@ -9,16 +9,16 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script>
|
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script>
|
||||||
{% comment %} <script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script> {% endcomment %}
|
{% comment %} <script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script> {% endcomment %}
|
||||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
{% comment %} <meta name="csrf-token" content="{{ csrf_token }}"> {% endcomment %}
|
||||||
|
|
||||||
<!-- FilePond CSS -->
|
<!-- FilePond CSS -->
|
||||||
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
|
{% comment %} <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
|
||||||
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
|
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body> {% endcomment %}
|
||||||
<header class="header">
|
{% comment %} <header class="header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex justify-content-between align-items-center py-3">
|
<div class="d-flex justify-content-between align-items-center py-3">
|
||||||
<div class="logo h4 mb-0">NorahUniversity ATS</div>
|
<div class="logo h4 mb-0">NorahUniversity ATS</div>
|
||||||
@ -37,8 +37,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header> {% endcomment %}
|
||||||
|
{% comment %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-light border-bottom">
|
<nav class="navbar navbar-expand-lg navbar-light border-bottom">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
@ -91,9 +91,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav> {% endcomment %}
|
||||||
|
|
||||||
<main class="container my-4">
|
{% comment %} <main class="container my-4">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
@ -107,12 +107,12 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main> {% endcomment %}
|
||||||
|
|
||||||
<!-- Delete Modal -->
|
<!-- Delete Modal -->
|
||||||
{% include 'includes/delete_modal.html' %}
|
{% comment %} {% include 'includes/delete_modal.html' %} {% endcomment %}
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
{% comment %} <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const csrfToken = "{{ csrf_token }}";
|
const csrfToken = "{{ csrf_token }}";
|
||||||
@ -138,4 +138,423 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %} {% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="King Abdullah Academic University Hospital - Applicant Tracking System">
|
||||||
|
<title>{% block title %}University ATS{% endblock %}</title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||||
|
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
|
||||||
|
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--kaauh-teal: #00636e;
|
||||||
|
--kaauh-teal-dark: #004a53;
|
||||||
|
--kaauh-light-bg: #f9fbfd;
|
||||||
|
--kaauh-border: #eaeff3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Top Bar === */
|
||||||
|
.top-bar {
|
||||||
|
background-color: white;
|
||||||
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
|
font-size: 0.825rem;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
}
|
||||||
|
.top-bar a { text-decoration: none; }
|
||||||
|
.top-bar .social-icons i {
|
||||||
|
color: var(--kaauh-teal);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.top-bar .social-icons i:hover {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
.top-bar .contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
.top-bar .logo-container img {
|
||||||
|
height: 50px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.top-bar .logo-container {
|
||||||
|
order: -1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.top-bar .contact-info {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Navbar === */
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.navbar-dark {
|
||||||
|
background-color: var(--kaauh-teal) !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.nav-link:hover,
|
||||||
|
.nav-link.active {
|
||||||
|
color: white !important;
|
||||||
|
background: rgba(255,255,255,0.12) !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.dropdown-menu {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
}
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: rgba(0, 99, 110, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
/* This hover effect is what overrides the click for desktop */
|
||||||
|
/* For the profile dropdown, we generally want the click behavior,
|
||||||
|
so let's make sure it doesn't have the desktop hover override.
|
||||||
|
The current CSS applies to all .dropdown:hover, which is fine,
|
||||||
|
but we should rely on Bootstrap's JS for this one. */
|
||||||
|
/* Removed unnecessary desktop hover override for this specific context
|
||||||
|
or ensured it works by click */
|
||||||
|
|
||||||
|
.dropdown:hover > .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
display: block !important;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(8px);
|
||||||
|
transition: opacity 0.25s, transform 0.25s;
|
||||||
|
}
|
||||||
|
.dropdown:hover .dropdown-menu {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Job Table === */
|
||||||
|
.job-table-wrapper {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.job-table thead th {
|
||||||
|
background: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.job-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.job-table tr:hover td {
|
||||||
|
background-color: rgba(0, 99, 110, 0.03);
|
||||||
|
}
|
||||||
|
.btn-apply {
|
||||||
|
background: var(--kaauh-teal);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-apply:hover {
|
||||||
|
background: var(--kaauh-teal-dark);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Footer & Alerts === */
|
||||||
|
.footer {
|
||||||
|
background: var(--kaauh-light-bg);
|
||||||
|
padding: 2rem 0;
|
||||||
|
border-top: 1px solid var(--kaauh-border);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
main.container {
|
||||||
|
min-height: calc(100vh - 220px);
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Profile Avatar === */
|
||||||
|
.profile-avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensures the dropdown-toggle style is minimal for the avatar */
|
||||||
|
.navbar-nav .dropdown-toggle.p-0::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block customCSS %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body class="d-flex flex-column min-vh-100">
|
||||||
|
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="container d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
|
<div class="d-flex align-items-center gap-3 social-icons">
|
||||||
|
<span class="text-muted d-none d-sm-inline">Follow Us:</span>
|
||||||
|
<a href="#" aria-label="Facebook"><i class="fab fa-facebook-f"></i></a>
|
||||||
|
<a href="#" aria-label="Twitter"><i class="fab fa-twitter"></i></a>
|
||||||
|
<a href="#" aria-label="Instagram"><i class="fab fa-instagram"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-info d-flex flex-wrap justify-content-center gap-2">
|
||||||
|
<div class="contact-item">
|
||||||
|
<i class="fas fa-envelope text-primary"></i>
|
||||||
|
<span class="d-none d-sm-inline">info@kaauh.edu.sa</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<i class="fas fa-phone text-primary"></i>
|
||||||
|
<span class="d-none d-sm-inline">+966 11 820 0000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logo-container d-flex gap-2">
|
||||||
|
<img src="{% static 'image/vision.svg' %}" alt="Saudi Vision 2030" loading="lazy">
|
||||||
|
<img src="{% static 'image/hospital_logo_3.png' %}" alt="King Abdullah Academic University Hospital" loading="lazy">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||||
|
<div class="container d-flex align-items-center">
|
||||||
|
<a class="navbar-brand text-white" href="#">
|
||||||
|
<i class="fas fa-hospital me-1"></i> KAAUH ATS
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" href="{% url 'dashboard' %}">
|
||||||
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
{% include "icons/dashboard.html" %}
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'job_list' %}active{% endif %}" href="{% url 'job_list' %}">
|
||||||
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
{% include "icons/jobs.html" %}
|
||||||
|
Jobs
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'candidate_list' %}active{% endif %}" href="{% url 'candidate_list' %}">
|
||||||
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
{% include "icons/users.html" %}
|
||||||
|
Candidates
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
|
||||||
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
Training
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}">
|
||||||
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
{% include "icons/meeting.html" %}
|
||||||
|
Meetings
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Meeting & Schedule</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar me-2"></i> Meetings</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-clock me-2"></i> Schedule</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Jobs</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-briefcase me-2"></i> Active Jobs</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-file-alt me-2"></i> Draft Jobs</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#"><i class="fas fa-list me-1"></i> Job List</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Candidates</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-users me-2"></i> All Candidates</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-user-plus me-2"></i> New Candidates</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">
|
||||||
|
<i class="fas fa-plus-circle me-1"></i> Create Job
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if not request.session.linkedin_authenticated %}
|
||||||
|
<ul class="navbar-nav ms-auto ms-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'linkedin_login' %}">
|
||||||
|
<i class="fab fa-linkedin me-1"></i> Connect LinkedIn
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<ul class="navbar-nav ms-auto ms-lg-0">
|
||||||
|
<li class="nav-item d-none d-lg-block">
|
||||||
|
<span class="nav-link text-success">
|
||||||
|
<i class="fab fa-linkedin me-1"></i> LinkedIn Connected
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="navbar-nav ms-2 ms-lg-3">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link p-0 dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<div class="profile-avatar">
|
||||||
|
{{ user.username|first|upper }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end py-2 shadow" style="min-width: 220px;">
|
||||||
|
<li class="px-3 py-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<div class="profile-avatar" style="width: 40px; height: 40px;">
|
||||||
|
{{ user.username|first|upper }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">{{ user.get_full_name|default:user.username }}</div>
|
||||||
|
<div class="text-muted small">{{ user.email|truncatechars:24 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider my-1"></li>
|
||||||
|
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-user-circle me-2 text-primary"></i> My Profile</a></li>
|
||||||
|
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-cog me-2 text-primary"></i> Settings</a></li>
|
||||||
|
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-history me-2 text-primary"></i> Activity Log</a></li>
|
||||||
|
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-question-circle me-2 text-primary"></i> Help & Support</a></li>
|
||||||
|
<li><hr class="dropdown-divider my-1"></li>
|
||||||
|
<li>
|
||||||
|
<form method="post" action="#" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="dropdown-item py-2 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt me-2"></i> Sign Out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</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="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
<!-- Delete Modal -->
|
||||||
|
{% include 'includes/delete_modal.html' %}
|
||||||
|
<footer class="footer mt-auto">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="mb-0">
|
||||||
|
© {% now "Y" %} King Abdullah Academic University Hospital (KAAUH).<br>
|
||||||
|
<small>All rights reserved.</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const navbarCollapse = document.getElementById('navbarNav');
|
||||||
|
const navLinks = navbarCollapse.querySelectorAll('.nav-link:not(.dropdown-toggle)');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
const bsCollapse = bootstrap.Collapse.getInstance(navbarCollapse);
|
||||||
|
if (bsCollapse && navbarCollapse.classList.contains('show')) {
|
||||||
|
bsCollapse.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% block customJS %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user