Compare commits
3 Commits
a8fae9011c
...
9bf0125121
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bf0125121 | |||
| 5983dc75ff | |||
| 3b26ed34fd |
6
.env
6
.env
@ -1,3 +1,3 @@
|
|||||||
DB_NAME=haikal_db
|
DB_NAME=norahuniversity
|
||||||
DB_USER=faheed
|
DB_USER=norahuniversity
|
||||||
DB_PASSWORD=Faheed@215
|
DB_PASSWORD=norahuniversity
|
||||||
9882
django.po.bkp
Normal file
9882
django.po.bkp
Normal file
File diff suppressed because it is too large
Load Diff
9885
django2.po
Normal file
9885
django2.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -269,7 +269,7 @@ class SourceAdvancedForm(forms.ModelForm):
|
|||||||
class PersonForm(forms.ModelForm):
|
class PersonForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Person
|
model = Person
|
||||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","address","gender"]
|
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","gender","address"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
|
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
|
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
@ -591,7 +591,6 @@ class JobPostingForm(forms.ModelForm):
|
|||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"placeholder": "Maximum number of applicants",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -834,9 +833,9 @@ class ProfileImageUploadForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class StaffUserCreationForm(UserCreationForm):
|
class StaffUserCreationForm(UserCreationForm):
|
||||||
email = forms.EmailField(required=True)
|
email = forms.EmailField(label=_("Email"), required=True)
|
||||||
first_name = forms.CharField(max_length=30, required=True)
|
first_name = forms.CharField(label=_("First Name"),max_length=30, required=True)
|
||||||
last_name = forms.CharField(max_length=150, required=True)
|
last_name = forms.CharField(label=_("Last Name"),max_length=150, required=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@ -1078,7 +1077,6 @@ class AgencyJobAssignmentForm(forms.ModelForm):
|
|||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"placeholder": "Maximum number of candidates",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"deadline_date": forms.DateTimeInput(
|
"deadline_date": forms.DateTimeInput(
|
||||||
@ -1090,7 +1088,6 @@ class AgencyJobAssignmentForm(forms.ModelForm):
|
|||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"rows": 3,
|
"rows": 3,
|
||||||
"placeholder": "Internal notes about this assignment",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
18
recruitment/migrations/0006_alter_customuser_email.py
Normal file
18
recruitment/migrations/0006_alter_customuser_email.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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,6 +45,12 @@ class CustomUser(AbstractUser):
|
|||||||
designation = models.CharField(
|
designation = models.CharField(
|
||||||
max_length=100, blank=True, null=True, verbose_name=_("Designation")
|
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:
|
class Meta:
|
||||||
verbose_name = _("User")
|
verbose_name = _("User")
|
||||||
@ -485,7 +491,6 @@ class Person(Base):
|
|||||||
unique=True,
|
unique=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name=_("Email"),
|
verbose_name=_("Email"),
|
||||||
help_text=_("Unique email address for the person"),
|
|
||||||
)
|
)
|
||||||
phone = models.CharField(
|
phone = models.CharField(
|
||||||
max_length=20, blank=True, null=True, verbose_name=_("Phone")
|
max_length=20, blank=True, null=True, verbose_name=_("Phone")
|
||||||
|
|||||||
@ -847,7 +847,10 @@ def kaauh_career(request):
|
|||||||
# job detail facing the candidate:
|
# job detail facing the candidate:
|
||||||
def application_detail(request, slug):
|
def application_detail(request, slug):
|
||||||
job = get_object_or_404(JobPosting, slug=slug)
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
return render(request, "applicant/application_detail.html", {"job": job})
|
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})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -1182,7 +1185,13 @@ def application_submit_form(request, template_slug):
|
|||||||
"""Display the form as a step-by-step wizard"""
|
"""Display the form as a step-by-step wizard"""
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return redirect("candidate_signup",slug=template_slug)
|
return redirect("candidate_signup",slug=template_slug)
|
||||||
|
|
||||||
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
|
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")
|
stage = template.stages.filter(name="Contact Information")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicantConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'applicant'
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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'}),
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
# 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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
# 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}"
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,103 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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'),
|
|
||||||
]
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
# 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})
|
|
||||||
@ -36,12 +36,22 @@
|
|||||||
<p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
|
<p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
|
||||||
|
|
||||||
{% if job.form_template %}
|
{% if job.form_template %}
|
||||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
|
{% 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">
|
||||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||||
</a>
|
</a>
|
||||||
{% elif not job.is_expired %}
|
{% elif not job.is_expired %}
|
||||||
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
|
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
|
||||||
{% endif %}
|
{% endif %} {% endcomment %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,7 +206,6 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@ -205,11 +214,17 @@
|
|||||||
|
|
||||||
{# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
|
{# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
|
||||||
{% if job.form_template %}
|
{% if job.form_template %}
|
||||||
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
|
<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">
|
{% if user.is_authenticated and already_applied %}
|
||||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
<button class="btn btn-main-action btn-lg w-100" disabled>
|
||||||
</a>
|
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
|
||||||
</footer>
|
</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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -123,14 +123,16 @@
|
|||||||
</li> {% endcomment %}
|
</li> {% endcomment %}
|
||||||
<li class="nav-item me-2">
|
<li class="nav-item me-2">
|
||||||
{% if LANGUAGE_CODE == 'en' %}
|
{% 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 }}">
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
||||||
<span class="me-2">🇸🇦</span> العربية
|
<span class="me-2">🇸🇦</span> العربية
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% elif LANGUAGE_CODE == 'ar' %}
|
{% 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 }}">
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
||||||
<span class="me-2">🇺🇸</span> English
|
<span class="me-2">🇺🇸</span> English
|
||||||
|
|||||||
@ -1249,7 +1249,7 @@ const elements = {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
state.templateId = result.template_slug;
|
state.templateId = result.template_slug;
|
||||||
window.location.href = "{% url 'form_templates_list' %}";
|
window.location.href = "{% url 'job_detail' template.job.slug %}";
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
alert('Error saving form template: ' + result.error);
|
alert('Error saving form template: ' + result.error);
|
||||||
|
|||||||
@ -361,7 +361,10 @@
|
|||||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if not job.form_template %}
|
<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 %}
|
||||||
<a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
<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" %}
|
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
|
||||||
</a>
|
</a>
|
||||||
@ -377,7 +380,7 @@
|
|||||||
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %} {% endcomment %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -276,7 +276,8 @@
|
|||||||
<th scope="col" rowspan="2">{% trans "Source" %}</th>
|
<th scope="col" rowspan="2">{% trans "Source" %}</th>
|
||||||
<th scope="col" rowspan="2">{% trans "Max Apps" %}</th>
|
<th scope="col" rowspan="2">{% trans "Max Apps" %}</th>
|
||||||
<th scope="col" rowspan="2">{% trans "Deadline" %}</th>
|
<th scope="col" rowspan="2">{% trans "Deadline" %}</th>
|
||||||
<th scope="col" rowspan="2">{% trans "Submission" %}</th>
|
<th scope="col" rowspan="2">{% trans "Assigned To" %}</th>
|
||||||
|
<th scope="col" rowspan="2"></th>
|
||||||
|
|
||||||
|
|
||||||
<th scope="col" colspan="6" class="candidate-management-header-title">
|
<th scope="col" colspan="6" class="candidate-management-header-title">
|
||||||
@ -306,11 +307,20 @@
|
|||||||
<td>{{ job.get_source }}</td>
|
<td>{{ job.get_source }}</td>
|
||||||
<td>{{ job.max_applications }}</td>
|
<td>{{ job.max_applications }}</td>
|
||||||
<td>{{ job.application_deadline|date:"d-m-Y" }}</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>
|
<td>
|
||||||
{% if job.form_template %}
|
{% if job.form_template %}
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<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' %}">
|
<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>
|
<i class="fas fa-file-alt text-primary-theme"></i>{% trans "Submissions" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static i18n crispy_forms_tags %}
|
{% load static i18n crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}Create Person - {{ block.super }}{% endblock %}
|
{% block title %}Create Applicant - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block customCSS %}
|
{% block customCSS %}
|
||||||
<style>
|
<style>
|
||||||
@ -184,9 +184,9 @@
|
|||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" id="person-form">
|
<form method="post" enctype="multipart/form-data" id="person-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{form|crispy}}
|
||||||
<!-- Profile Image Section -->
|
<!-- Profile Image Section -->
|
||||||
<div class="row mb-4">
|
{% comment %} <div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
|
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
|
||||||
<div id="image-preview-container">
|
<div id="image-preview-container">
|
||||||
@ -261,7 +261,7 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
{{ form.address }}
|
{{ form.address }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
|
|
||||||
<!-- LinkedIn Profile Section -->
|
<!-- LinkedIn Profile Section -->
|
||||||
{% comment %} <div class="row mb-4">
|
{% comment %} <div class="row mb-4">
|
||||||
@ -292,11 +292,8 @@
|
|||||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||||
</a>
|
</a>
|
||||||
<div class="d-flex gap-2">
|
<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">
|
<button type="submit" class="btn btn-main-action">
|
||||||
<i class="fas fa-save me-1"></i> {% trans "Create Person" %}
|
<i class="fas fa-save me-1"></i> {% trans "Create Applicant" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -308,141 +305,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% comment %} <li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="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' %}">
|
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
||||||
<i class="fas fa-globe me-1"></i>
|
<i class="fas fa-globe me-1"></i>
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||||
<i class="fas fa-tasks me-2"></i>
|
<i class="fas fa-tasks me-2"></i>
|
||||||
{{ title }}
|
{% trans "Create New Assignment" %}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
{% trans "Assign a job to an external hiring agency" %}
|
{% trans "Assign a job to an external hiring agency" %}
|
||||||
@ -213,7 +213,7 @@
|
|||||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="btn btn-main-action">
|
<button type="submit" class="btn btn-main-action">
|
||||||
<i class="fas fa-save me-1"></i> {{ button_text }}
|
<i class="fas fa-save me-1"></i> {% trans "Create Assignment" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -279,7 +279,7 @@
|
|||||||
<th scope="col" >{% trans "Major" %}</th>
|
<th scope="col" >{% trans "Major" %}</th>
|
||||||
<th scope="col" >{% trans "Stage" %}</th>
|
<th scope="col" >{% trans "Stage" %}</th>
|
||||||
<th scope="col">{% trans "Hiring Source" %}</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>
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
|
|
||||||
<h2 id="form-title" class="h3 fw-bold mb-4 text-center">
|
<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 Staff User" %}
|
<i class="fas fa-user-plus me-2 text-accent"></i>{% trans "Create User" %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@ -66,7 +66,7 @@
|
|||||||
--bs-btn-active-border-color: #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;">
|
--bs-btn-color: #ffffff;">
|
||||||
<i class="fas fa-save me-2"></i>{% trans "Create Staff User" %}
|
<i class="fas fa-save me-2"></i>{% trans "Create User" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user