Compare commits
No commits in common. "9bf01251211617050ed13c3bb46a63c1d0220249" and "a8fae9011c6be6e6a9bd557603ebcb72410fdbd7" have entirely different histories.
9bf0125121
...
a8fae9011c
6
.env
6
.env
@ -1,3 +1,3 @@
|
||||
DB_NAME=norahuniversity
|
||||
DB_USER=norahuniversity
|
||||
DB_PASSWORD=norahuniversity
|
||||
DB_NAME=haikal_db
|
||||
DB_USER=faheed
|
||||
DB_PASSWORD=Faheed@215
|
||||
9882
django.po.bkp
9882
django.po.bkp
File diff suppressed because it is too large
Load Diff
9885
django2.po
9885
django2.po
File diff suppressed because it is too large
Load Diff
@ -269,7 +269,7 @@ class SourceAdvancedForm(forms.ModelForm):
|
||||
class PersonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","gender","address"]
|
||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","address","gender"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@ -591,6 +591,7 @@ class JobPostingForm(forms.ModelForm):
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"min": 1,
|
||||
"placeholder": "Maximum number of applicants",
|
||||
}
|
||||
),
|
||||
}
|
||||
@ -833,9 +834,9 @@ class ProfileImageUploadForm(forms.ModelForm):
|
||||
|
||||
|
||||
class StaffUserCreationForm(UserCreationForm):
|
||||
email = forms.EmailField(label=_("Email"), required=True)
|
||||
first_name = forms.CharField(label=_("First Name"),max_length=30, required=True)
|
||||
last_name = forms.CharField(label=_("Last Name"),max_length=150, required=True)
|
||||
email = forms.EmailField(required=True)
|
||||
first_name = forms.CharField(max_length=30, required=True)
|
||||
last_name = forms.CharField(max_length=150, required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@ -1077,6 +1078,7 @@ class AgencyJobAssignmentForm(forms.ModelForm):
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"min": 1,
|
||||
"placeholder": "Maximum number of candidates",
|
||||
}
|
||||
),
|
||||
"deadline_date": forms.DateTimeInput(
|
||||
@ -1088,6 +1090,7 @@ class AgencyJobAssignmentForm(forms.ModelForm):
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"rows": 3,
|
||||
"placeholder": "Internal notes about this assignment",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-23 12:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0005_alter_interviewschedule_template_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='email',
|
||||
field=models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True),
|
||||
),
|
||||
]
|
||||
@ -45,12 +45,6 @@ class CustomUser(AbstractUser):
|
||||
designation = models.CharField(
|
||||
max_length=100, blank=True, null=True, verbose_name=_("Designation")
|
||||
)
|
||||
email = models.EmailField(
|
||||
unique=True,
|
||||
error_messages={
|
||||
"unique": _("A user with this email already exists."),
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
@ -251,7 +245,7 @@ class JobPosting(Base):
|
||||
)
|
||||
# Field to store the generated zip file
|
||||
cv_zip_file = models.FileField(upload_to='job_zips/', null=True, blank=True)
|
||||
|
||||
|
||||
# Field to track if the background task has completed
|
||||
zip_created = models.BooleanField(default=False)
|
||||
|
||||
@ -491,6 +485,7 @@ class Person(Base):
|
||||
unique=True,
|
||||
db_index=True,
|
||||
verbose_name=_("Email"),
|
||||
help_text=_("Unique email address for the person"),
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=20, blank=True, null=True, verbose_name=_("Phone")
|
||||
@ -997,14 +992,14 @@ class Application(Base):
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||
return Document.objects.filter(content_type=content_type, object_id=self.id)
|
||||
|
||||
|
||||
@property
|
||||
def belong_to_an_agency(self):
|
||||
if self.hiring_agency:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
deadline=self.job.application_deadline
|
||||
@ -1013,7 +1008,7 @@ class Application(Base):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1182,7 +1177,7 @@ class OnsiteLocationDetails(InterviewLocation):
|
||||
verbose_name_plural = _("Onsite Location Details")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# --- 2. Scheduling Models ---
|
||||
|
||||
@ -760,7 +760,7 @@ from django.utils.html import strip_tags
|
||||
|
||||
def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):
|
||||
"""Internal helper to create and send a single email."""
|
||||
|
||||
|
||||
|
||||
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
|
||||
is_html = '<' in body_message and '>' in body_message
|
||||
@ -782,7 +782,7 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
|
||||
result=email_obj.send(fail_silently=False)
|
||||
|
||||
if result==1 and sender and job: # job is none when email sent after message creation
|
||||
|
||||
|
||||
try:
|
||||
user=get_object_or_404(User,email=recipient)
|
||||
new_message = Message.objects.create(
|
||||
@ -866,7 +866,7 @@ def generate_and_save_cv_zip(job_posting_id):
|
||||
"""
|
||||
job = JobPosting.objects.get(id=job_posting_id)
|
||||
entries = Application.objects.filter(job=job)
|
||||
|
||||
|
||||
zip_buffer = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
@ -892,7 +892,7 @@ def generate_and_save_cv_zip(job_posting_id):
|
||||
zip_buffer.seek(0)
|
||||
now = str(timezone.now())
|
||||
zip_filename = f"all_cvs_for_{job.slug}_{job.title}_{now}.zip"
|
||||
|
||||
|
||||
# Use ContentFile to save the bytes stream into the FileField
|
||||
job.cv_zip_file.save(zip_filename, ContentFile(zip_buffer.read()))
|
||||
job.zip_created = True # Assuming you added a BooleanField for tracking completion
|
||||
|
||||
@ -847,10 +847,7 @@ def kaauh_career(request):
|
||||
# job detail facing the candidate:
|
||||
def application_detail(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
already_applied = False
|
||||
if request.user.is_authenticated:
|
||||
already_applied = Application.objects.filter(job=job,person=request.user.person_profile).exists()
|
||||
return render(request, "applicant/application_detail.html", {"job": job,"already_applied":already_applied})
|
||||
return render(request, "applicant/application_detail.html", {"job": job})
|
||||
|
||||
|
||||
@login_required
|
||||
@ -1185,13 +1182,7 @@ def application_submit_form(request, template_slug):
|
||||
"""Display the form as a step-by-step wizard"""
|
||||
if not request.user.is_authenticated:
|
||||
return redirect("candidate_signup",slug=template_slug)
|
||||
|
||||
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
|
||||
|
||||
if Application.objects.filter(job=template.job,person=request.user.person_profile).exists():
|
||||
messages.error(request, _("You have already submitted an application for this job."))
|
||||
return redirect("application_detail",slug=template.job.slug)
|
||||
|
||||
stage = template.stages.filter(name="Contact Information")
|
||||
|
||||
|
||||
|
||||
0
static/image/applicant/__init__.py
Normal file
0
static/image/applicant/__init__.py
Normal file
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
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
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})
|
||||
@ -2,31 +2,31 @@
|
||||
{% load static i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🚀 TOP NAV BAR (Sticky and Themed) #}
|
||||
{# ------------------------------------------------ #}
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top border-bottom"
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top border-bottom"
|
||||
style="background-color: var(--kaauh-teal); z-index: 1030; height: 50px;">
|
||||
<div class="container-fluid container-lg">
|
||||
<span class="navbar-text text-white fw-bold fs-6">{% trans "Job Overview" %}</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🔔 DJANGO MESSAGES (Refined placement and styling) #}
|
||||
{# ------------------------------------------------ #}
|
||||
|
||||
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 💻 MAIN CONTENT CONTAINER #}
|
||||
{# ------------------------------------------------ #}
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="row g-4 main-content-area">
|
||||
|
||||
|
||||
{# 📌 RIGHT COLUMN: Sticky Apply Card (Desktop Only) #}
|
||||
<div class="col-lg-4 order-lg-2 d-none d-lg-block">
|
||||
<div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
|
||||
<div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
|
||||
<div class="card-header bg-white border-bottom p-3">
|
||||
<h5 class="mb-0 fw-bold text-kaauh-teal">
|
||||
<i class="fas fa-file-signature me-2"></i>{% trans "Ready to Apply?" %}
|
||||
@ -34,24 +34,14 @@
|
||||
</div>
|
||||
<div class="card-body text-center p-4">
|
||||
<p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
|
||||
|
||||
|
||||
{% if job.form_template %}
|
||||
{% if user.is_authenticated and already_applied %}
|
||||
<button class="btn btn-main-action btn-lg w-100" disabled>
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% comment %} <a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% elif not job.is_expired %}
|
||||
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
|
||||
{% endif %} {% endcomment %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -59,32 +49,32 @@
|
||||
{# 📝 LEFT COLUMN: Job Details #}
|
||||
<div class="col-lg-8 order-lg-1">
|
||||
<article class="card shadow-lg border-0">
|
||||
|
||||
|
||||
{# Job Title Header #}
|
||||
<header class="card-header bg-white border-bottom p-4">
|
||||
<h1 class="h2 mb-0 fw-bolder text-kaauh-teal">{{ job.title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="card-body p-4">
|
||||
|
||||
|
||||
<h4 class="mb-4 fw-bold text-muted border-bottom pb-2">{% trans "Summary" %}</h4>
|
||||
|
||||
|
||||
{# Job Metadata/Overview Grid #}
|
||||
<section class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary p-3 rounded bg-light-subtle border">
|
||||
|
||||
|
||||
{# SALARY #}
|
||||
{% if job.salary_range %}
|
||||
<div class="col">
|
||||
<i class="fas fa-money-bill-wave text-success me-2 fa-fw"></i>
|
||||
<strong>{% trans "Salary:" %}</strong>
|
||||
<strong>{% trans "Salary:" %}</strong>
|
||||
<span class="fw-bold text-success">{{ job.salary_range }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# DEADLINE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-calendar-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Deadline:" %}</strong>
|
||||
<strong>{% trans "Deadline:" %}</strong>
|
||||
{% if job.application_deadline %}
|
||||
<time datetime="{{ job.application_deadline|date:'Y-m-d' }}">
|
||||
{{ job.application_deadline|date:"M d, Y" }}
|
||||
@ -96,50 +86,50 @@
|
||||
<span class="text-muted">{% trans "Ongoing" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{# JOB TYPE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
<div class="col">
|
||||
<i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
|
||||
|
||||
{# LOCATION #}
|
||||
<div class="col">
|
||||
<i class="fas fa-map-marker-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
<div class="col">
|
||||
<i class="fas fa-map-marker-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
|
||||
|
||||
{# DEPARTMENT #}
|
||||
<div class="col">
|
||||
<i class="fas fa-building text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
<div class="col">
|
||||
<i class="fas fa-building text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
|
||||
{# JOB ID #}
|
||||
<div class="col">
|
||||
<i class="fas fa-hashtag text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }}
|
||||
<div class="col">
|
||||
<i class="fas fa-hashtag text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
|
||||
{# WORKPLACE TYPE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-laptop-house text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
<div class="col">
|
||||
<i class="fas fa-laptop-house text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
{# Detailed Accordion Section #}
|
||||
<div class="accordion accordion-flush" id="jobDetailAccordion">
|
||||
|
||||
|
||||
{% with active_collapse="collapseOne" %}
|
||||
|
||||
|
||||
{# JOB DESCRIPTION #}
|
||||
{% if job.has_description_content %}
|
||||
<div class="accordion-item border-top border-bottom">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#{{ active_collapse }}" aria-expanded="true"
|
||||
<button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#{{ active_collapse }}" aria-expanded="true"
|
||||
aria-controls="{{ active_collapse }}">
|
||||
<i class="fas fa-info-circle me-3 fa-fw"></i> {% trans "Job Description" %}
|
||||
</button>
|
||||
@ -151,12 +141,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# QUALIFICATIONS #}
|
||||
{% if job.has_qualifications_content %}
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||
<i class="fas fa-graduation-cap me-3 fa-fw"></i> {% trans "Qualifications" %}
|
||||
</button>
|
||||
@ -168,12 +158,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# BENEFITS #}
|
||||
{% if job.has_benefits_content %}
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||
<i class="fas fa-hand-holding-usd me-3 fa-fw"></i> {% trans "Benefits" %}
|
||||
</button>
|
||||
@ -190,7 +180,7 @@
|
||||
{% if job.has_application_instructions_content %}
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingFour">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
|
||||
<i class="fas fa-file-alt me-3 fa-fw"></i> {% trans "Application Instructions" %}
|
||||
</button>
|
||||
@ -202,30 +192,25 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
|
||||
{% if job.form_template %}
|
||||
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
|
||||
{% if user.is_authenticated and already_applied %}
|
||||
<button class="btn btn-main-action btn-lg w-100" disabled>
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endblock content%}
|
||||
@ -123,16 +123,14 @@
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item me-2">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
||||
<span class="me-2">🇸🇦</span> العربية
|
||||
</button>
|
||||
</form>
|
||||
{% elif LANGUAGE_CODE == 'ar' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
@ -314,7 +312,7 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
{% comment %} <li class="nav-item dropdown ms-lg-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
||||
|
||||
@ -1249,7 +1249,7 @@ const elements = {
|
||||
|
||||
if (result.success) {
|
||||
state.templateId = result.template_slug;
|
||||
window.location.href = "{% url 'job_detail' template.job.slug %}";
|
||||
window.location.href = "{% url 'form_templates_list' %}";
|
||||
|
||||
} else {
|
||||
alert('Error saving form template: ' + result.error);
|
||||
|
||||
@ -331,15 +331,15 @@
|
||||
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
|
||||
</a>
|
||||
|
||||
|
||||
<a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fa-solid fa-download me-1"></i> {% trans "Generate All CVs" %}
|
||||
</a>
|
||||
|
||||
|
||||
<a href="{% url 'download_ready_cvs' job.slug %}" class="btn btn-outline-primary">
|
||||
<i class="fa-solid fa-eye me-1"></i> {% trans "View All CVs" %}
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -361,10 +361,7 @@
|
||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||
</p>
|
||||
|
||||
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Job Form" %}
|
||||
</a>
|
||||
{% comment %} {% if not job.form_template %}
|
||||
{% if not job.form_template %}
|
||||
<a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
|
||||
</a>
|
||||
@ -380,7 +377,7 @@
|
||||
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %} {% endcomment %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-info: #17a2b8;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa; /* Subtle light background */
|
||||
font-family: 'Inter', sans-serif;
|
||||
@ -106,7 +106,7 @@
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f0f4f7;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* OPTIMIZED MAIN TABLE COLUMN WIDTHS (Total must be 100%)
|
||||
* --------------------------------------------------------
|
||||
@ -155,12 +155,12 @@
|
||||
font-size: 0.9rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Strong visual separator before metrics data */
|
||||
.table tbody tr td:nth-child(6) {
|
||||
border-left: 2px solid var(--kaauh-teal-dark) !important;
|
||||
border-left: 2px solid var(--kaauh-teal-dark) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Subtle vertical lines between metric data cells */
|
||||
.table tbody td.candidate-data-cell:not(:first-child) {
|
||||
border-left: 1px solid #f0f4f7;
|
||||
@ -207,7 +207,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
|
||||
{# --- MAIN HEADER AND ACTION BUTTON --- #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
@ -276,9 +276,8 @@
|
||||
<th scope="col" rowspan="2">{% trans "Source" %}</th>
|
||||
<th scope="col" rowspan="2">{% trans "Max Apps" %}</th>
|
||||
<th scope="col" rowspan="2">{% trans "Deadline" %}</th>
|
||||
<th scope="col" rowspan="2">{% trans "Assigned To" %}</th>
|
||||
<th scope="col" rowspan="2"></th>
|
||||
|
||||
<th scope="col" rowspan="2">{% trans "Submission" %}</th>
|
||||
|
||||
|
||||
<th scope="col" colspan="6" class="candidate-management-header-title">
|
||||
{% trans "Applicants Metrics (Current Stage Count)" %}
|
||||
@ -307,28 +306,19 @@
|
||||
<td>{{ job.get_source }}</td>
|
||||
<td>{{ job.max_applications }}</td>
|
||||
<td>{{ job.application_deadline|date:"d-m-Y" }}</td>
|
||||
{% if job.assigned_to %}
|
||||
<td>
|
||||
<span class="badge bg-primary-theme">
|
||||
{{ job.assigned_to.first_name|capfirst }} {{ job.assigned_to.last_name|capfirst }}
|
||||
</span>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{% trans "Unassigned" %}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if job.form_template %}
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'All Application Submissions' %}">
|
||||
<i class="fas fa-file-alt text-primary-theme"></i>{% trans "Submissions" %}
|
||||
<i class="fas fa-file-alt text-primary-theme"></i>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted small"></span>
|
||||
{% endif%}
|
||||
</td>
|
||||
|
||||
|
||||
{# CANDIDATE MANAGEMENT DATA #}
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
@ -376,7 +366,7 @@
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Job Details" %}
|
||||
</a>
|
||||
<div class="btn-group btn-group-sm">
|
||||
|
||||
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Submissions' %}">
|
||||
<i class="fas fa-file-alt me-1"></i>{% trans "Submissions" %}
|
||||
@ -392,9 +382,9 @@
|
||||
{# --- END CARD VIEW --- #}
|
||||
</div>
|
||||
{# --- END OF JOB LIST CONTAINER --- #}
|
||||
|
||||
|
||||
{% include "includes/paginator.html" %}
|
||||
|
||||
|
||||
{% if not jobs and not job_list_data and not page_obj %}
|
||||
<div class="text-center py-5 card shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
|
||||
{% block title %}Create Applicant - {{ block.super }}{% endblock %}
|
||||
{% block title %}Create Person - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -184,9 +184,9 @@
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="person-form">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
|
||||
<!-- Profile Image Section -->
|
||||
{% comment %} <div class="row mb-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
|
||||
<div id="image-preview-container">
|
||||
@ -261,7 +261,7 @@
|
||||
<div class="col-12">
|
||||
{{ form.address }}
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
|
||||
<!-- LinkedIn Profile Section -->
|
||||
{% comment %} <div class="row mb-4">
|
||||
@ -292,8 +292,11 @@
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="reset" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-undo me-1"></i> {% trans "Reset" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Applicant" %}
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Person" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -305,3 +308,141 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Profile Image Preview
|
||||
const profileImageInput = document.getElementById('id_profile_image');
|
||||
const imagePreviewContainer = document.getElementById('image-preview-container');
|
||||
|
||||
profileImageInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
imagePreviewContainer.innerHTML = `
|
||||
<img src="${e.target.result}" alt="Profile Preview" class="profile-image-preview">
|
||||
<h5 class="text-muted mt-3">${file.name}</h5>
|
||||
<p class="text-muted small">{% trans "Click to change photo" %}</p>
|
||||
`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Form Validation
|
||||
const form = document.getElementById('person-form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Basic validation
|
||||
const firstName = document.getElementById('id_first_name').value.trim();
|
||||
const lastName = document.getElementById('id_last_name').value.trim();
|
||||
const email = document.getElementById('id_email').value.trim();
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "First name and last name are required." %}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (email && !isValidEmail(email)) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Please enter a valid email address." %}');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Email validation helper
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// LinkedIn URL validation
|
||||
const linkedinInput = document.getElementById('id_linkedin_profile');
|
||||
linkedinInput.addEventListener('blur', function() {
|
||||
const value = this.value.trim();
|
||||
if (value && !isValidLinkedInURL(value)) {
|
||||
this.classList.add('is-invalid');
|
||||
if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
feedback.textContent = '{% trans "Please enter a valid LinkedIn URL" %}';
|
||||
this.parentNode.appendChild(feedback);
|
||||
}
|
||||
} else {
|
||||
this.classList.remove('is-invalid');
|
||||
const feedback = this.parentNode.querySelector('.invalid-feedback');
|
||||
if (feedback) feedback.remove();
|
||||
}
|
||||
});
|
||||
|
||||
function isValidLinkedInURL(url) {
|
||||
const linkedinRegex = /^https?:\/\/(www\.)?linkedin\.com\/.+/i;
|
||||
return linkedinRegex.test(url);
|
||||
}
|
||||
|
||||
// Drag and Drop functionality
|
||||
const uploadArea = document.querySelector('.profile-image-upload');
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
function highlight(e) {
|
||||
uploadArea.style.borderColor = 'var(--kaauh-teal)';
|
||||
uploadArea.style.backgroundColor = 'var(--kaauh-gray-light)';
|
||||
}
|
||||
|
||||
function unhighlight(e) {
|
||||
uploadArea.style.borderColor = 'var(--kaauh-border)';
|
||||
uploadArea.style.backgroundColor = 'transparent';
|
||||
}
|
||||
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length > 0) {
|
||||
profileImageInput.files = files;
|
||||
const event = new Event('change', { bubbles: true });
|
||||
profileImageInput.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.select2').select2();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
|
||||
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
{% comment %} <li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
||||
<i class="fas fa-globe me-1"></i>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa; /* Light background for better contrast */
|
||||
}
|
||||
@ -45,7 +45,7 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Secondary Action Button (Teal Outline) */
|
||||
.btn-outline-primary-teal {
|
||||
color: var(--kaauh-teal);
|
||||
@ -69,7 +69,7 @@
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
|
||||
|
||||
/* Applying Bootstrap classes to Django fields if not done in the form definition */
|
||||
.kaauh-field-control > input,
|
||||
.kaauh-field-control > textarea,
|
||||
@ -104,7 +104,7 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-tasks me-2"></i>
|
||||
{% trans "Create New Assignment" %}
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "Assign a job to an external hiring agency" %}
|
||||
@ -120,22 +120,22 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.agency.id_for_label }}" class="form-label">
|
||||
{{ form.agency.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{# Wrapper Div for styling consistency (Assumes agency is a SELECT field) #}
|
||||
<div class="kaauh-field-control">
|
||||
{{ form.agency|attr:'class:form-select' }}
|
||||
{{ form.agency|attr:'class:form-select' }}
|
||||
</div>
|
||||
{% if form.agency.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{% for error in form.agency.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.job.id_for_label }}" class="form-label">
|
||||
{{ form.job.label }} <span class="text-danger">*</span>
|
||||
@ -213,7 +213,7 @@
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Assignment" %}
|
||||
<i class="fas fa-save me-1"></i> {{ button_text }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -227,10 +227,10 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- Consistency Check: Ensure Django widgets have the Bootstrap classes ---
|
||||
// If your form fields are NOT already adding classes via widget attrs in the Django form,
|
||||
// If your form fields are NOT already adding classes via widget attrs in the Django form,
|
||||
// you MUST add the following utility filter to your project to make this template work:
|
||||
// `|attr:'class:form-control'`
|
||||
|
||||
|
||||
// Auto-populate agency field when job is selected
|
||||
const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
|
||||
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
--kaauh-success: #28a745;
|
||||
--kaauh-danger: #dc3545;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@ -146,9 +146,9 @@
|
||||
/* ------------------------------------------- */
|
||||
.kaats-spinner {
|
||||
animation: kaats-spinner-rotate 1.5s linear infinite;
|
||||
width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -160,7 +160,7 @@
|
||||
.kaats-spinner circle {
|
||||
stroke: var(--kaauh-border, #e9ecef);
|
||||
fill: none;
|
||||
stroke-width: 5;
|
||||
stroke-width: 5;
|
||||
}
|
||||
|
||||
@keyframes kaats-spinner-rotate {
|
||||
@ -225,14 +225,14 @@
|
||||
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="stage_filter" class="form-label small text-muted">{% trans "Filter by Stages" %}</label>
|
||||
<div class="d-flex gap-2">
|
||||
|
||||
|
||||
<select name="stage" id="stage_filter" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
<option value="Applied" {% if stage_filter == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
|
||||
@ -279,7 +279,7 @@
|
||||
<th scope="col" >{% trans "Major" %}</th>
|
||||
<th scope="col" >{% trans "Stage" %}</th>
|
||||
<th scope="col">{% trans "Hiring Source" %}</th>
|
||||
<th scope="col" >{% trans "Created At" %}</th>
|
||||
<th scope="col" >{% trans "created At" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -365,11 +365,11 @@
|
||||
<span class="d-block mb-1"><i class="fas fa-envelope"></i> {{ candidate.email }}</span>
|
||||
<span class="d-block mb-1"><i class="fas fa-phone-alt"></i> {{ candidate.phone|default:"N/A" }}</span>
|
||||
<span class="d-block mb-1"><i class="fas fa-calendar-alt"></i> {{ candidate.created_at|date:"d-m-Y" }}</span>
|
||||
<span class="d-block mt-2"><i class="fas fa-briefcase"></i>
|
||||
<span class="d-block mt-2"><i class="fas fa-briefcase"></i>
|
||||
<span class="badge bg-primary"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-decoration-none text-white">{{ candidate.job.title }}</a></span>
|
||||
</span>
|
||||
{% if candidate.hiring_agency %}
|
||||
<span class="d-block mt-2"><i class="fas fa-building"></i>
|
||||
<span class="d-block mt-2"><i class="fas fa-building"></i>
|
||||
<a href="{% url 'agency_detail' candidate.hiring_agency.slug %}" class="text-decoration-none">
|
||||
<span class="badge bg-info">{{ candidate.hiring_agency.name }}</span>
|
||||
</a>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<style>
|
||||
/* Custom styles for card and text accent */
|
||||
.form-card {
|
||||
max-width: 500px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
@ -23,7 +23,7 @@
|
||||
.text-accent:hover {
|
||||
color: #004d55 !important; /* Darker teal hover */
|
||||
}
|
||||
|
||||
|
||||
/* Removed aggressive !important button overrides from here */
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -34,7 +34,7 @@
|
||||
<div class="form-card">
|
||||
|
||||
<h2 id="form-title" class="h3 fw-bold mb-4 text-center">
|
||||
<i class="fas fa-user-plus me-2 text-accent"></i>{% trans "Create User" %}
|
||||
<i class="fas fa-user-plus me-2 text-accent"></i>{% trans "Create Staff User" %}
|
||||
</h2>
|
||||
|
||||
{% if messages %}
|
||||
@ -56,17 +56,17 @@
|
||||
{% endif %}
|
||||
|
||||
{# 🚀 GUARANTEED FIX: Using inline CSS variables to override Bootstrap Primary color #}
|
||||
<button type="submit"
|
||||
<button type="submit"
|
||||
class="btn btn-primary w-100 mt-3"
|
||||
style="--bs-btn-bg: #007a88;
|
||||
style="--bs-btn-bg: #007a88;
|
||||
--bs-btn-border-color: #007a88;
|
||||
--bs-btn-hover-bg: #004d55;
|
||||
--bs-btn-hover-border-color: #004d55;
|
||||
--bs-btn-active-bg: #004d55;
|
||||
--bs-btn-active-border-color: #004d55;
|
||||
--bs-btn-focus-shadow-rgb: 40, 167, 69;
|
||||
--bs-btn-focus-shadow-rgb: 40, 167, 69;
|
||||
--bs-btn-color: #ffffff;">
|
||||
<i class="fas fa-save me-2"></i>{% trans "Create User" %}
|
||||
<i class="fas fa-save me-2"></i>{% trans "Create Staff User" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user