new changes removed the encryption

This commit is contained in:
Faheed 2025-12-08 19:55:21 +03:00
parent c83b878be6
commit d2a86f12c3
15 changed files with 499 additions and 170 deletions

View File

@ -62,7 +62,7 @@ INSTALLED_APPS = [
"django_q",
"widget_tweaks",
"easyaudit",
"encrypted_model_fields",
"mathfilters"
]
@ -534,4 +534,3 @@ LOGGING={
}
FIELD_ENCRYPTION_KEY="PWQimxxcDjlRsSSof2gaj42a3frmrLt2xgCTa4R06pE="

View File

@ -285,6 +285,42 @@ class PersonForm(forms.ModelForm):
"gpa": forms.TextInput(attrs={'class': 'custom-decimal-input'}),
"national_id":forms.NumberInput(attrs={'min': 0, 'step': 1}),
}
def clean_email(self):
email = self.cleaned_data.get('email')
if not email:
return email
if email:
instance = self.instance
qs = CustomUser.objects.filter(email=email) | CustomUser.objects.filter(username=email)
if not instance.pk: # Creating new instance
if qs.exists():
raise ValidationError(_("A user account with this email address already exists. Please use a different email."))
else: # Editing existing instance
# if (
# qs
# .exclude(pk=instance.user.pk)
# .exists()
# ):
# raise ValidationError(_("An user with this email already exists."))
pass
return email.strip()
return email
class ApplicationForm(forms.ModelForm):
class Meta:
@ -987,7 +1023,7 @@ class HiringAgencyForm(forms.ModelForm):
}
),
"email": forms.EmailInput(
attrs={"class": "form-control"}
attrs={"class": "form-control","required": True}
),
"phone": forms.TextInput(
attrs={"class": "form-control"}
@ -1076,6 +1112,7 @@ class HiringAgencyForm(forms.ModelForm):
# instance = self.instance
email = email.lower().strip()
if not instance.pk: # Creating new instance
print("created ....")
if HiringAgency.objects.filter(email=email).exists():
raise ValidationError("An agency with this email already exists.")
else: # Editing existing instance

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-12-07 13:15
# Generated by Django 5.2.7 on 2025-12-08 15:04
import django.contrib.auth.models
import django.contrib.auth.validators
@ -8,7 +8,6 @@ import django.utils.timezone
import django_ckeditor_5.fields
import django_countries.fields
import django_extensions.db.fields
import encrypted_model_fields.fields
import recruitment.validators
from django.conf import settings
from django.db import migrations, models
@ -153,10 +152,10 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')),
('phone', encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone')),
('phone', models.CharField(blank=True, null=True, verbose_name='Phone')),
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')),
('email', encrypted_model_fields.fields.EncryptedEmailField(error_messages={'unique': 'A user with this email already exists.'}, unique=True)),
('email', models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
@ -245,8 +244,8 @@ class Migration(migrations.Migration):
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('email', models.EmailField(max_length=254, unique=True)),
('phone', models.CharField(blank=True, max_length=20, null=True)),
('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
@ -487,14 +486,14 @@ class Migration(migrations.Migration):
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')),
('email', encrypted_model_fields.fields.EncryptedEmailField(db_index=True, unique=True, verbose_name='Email')),
('phone', encrypted_model_fields.fields.EncryptedPositiveIntegerField(blank=True, null=True, verbose_name='Phone')),
('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
('phone', models.CharField(blank=True, null=True, verbose_name='Phone')),
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')),
('gpa', models.DecimalField(decimal_places=2, help_text='GPA must be between 0 and 4.', max_digits=3, verbose_name='GPA')),
('national_id', encrypted_model_fields.fields.EncryptedPositiveIntegerField(help_text='Enter the national id or iqama number')),
('national_id', models.CharField(help_text='Enter the national id or iqama number')),
('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')),
('address', encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Address')),
('address', models.TextField(blank=True, null=True, verbose_name='Address')),
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-12-07 13:38
import encrypted_model_fields.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='person',
name='address',
field=encrypted_model_fields.fields.EncryptedTextField(blank=True, null=True, verbose_name='Address'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 5.2.7 on 2025-12-07 13:43
import encrypted_model_fields.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_alter_person_address'),
]
operations = [
migrations.AlterField(
model_name='person',
name='national_id',
field=encrypted_model_fields.fields.EncryptedCharField(help_text='Enter the national id or iqama number'),
),
migrations.AlterField(
model_name='person',
name='phone',
field=encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone'),
),
]

View File

@ -1,39 +0,0 @@
# Generated by Django 5.2.7 on 2025-12-07 13:59
import encrypted_model_fields.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_alter_person_national_id_alter_person_phone'),
]
operations = [
migrations.AlterField(
model_name='formsubmission',
name='applicant_email',
field=encrypted_model_fields.fields.EncryptedEmailField(blank=True, db_index=True, help_text='Email of the applicant'),
),
migrations.AlterField(
model_name='hiringagency',
name='email',
field=encrypted_model_fields.fields.EncryptedEmailField(blank=True),
),
migrations.AlterField(
model_name='hiringagency',
name='phone',
field=encrypted_model_fields.fields.EncryptedCharField(blank=True),
),
migrations.AlterField(
model_name='participants',
name='email',
field=encrypted_model_fields.fields.EncryptedEmailField(verbose_name='Email'),
),
migrations.AlterField(
model_name='participants',
name='phone',
field=encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone Number'),
),
]

View File

@ -21,7 +21,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Value, IntegerField, CharField,Q
from django.db.models.functions import Coalesce, Cast
from django.db.models.fields.json import KeyTransform, KeyTextTransform
from encrypted_model_fields.fields import EncryptedCharField,EncryptedEmailField,EncryptedTextField
class EmailContent(models.Model):
subject = models.CharField(max_length=255, verbose_name=_("Subject"))
@ -47,7 +47,7 @@ class CustomUser(AbstractUser):
user_type = models.CharField(
max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type")
)
phone = EncryptedCharField(
phone = models.CharField(
blank=True, null=True, verbose_name=_("Phone")
)
profile_image = models.ImageField(
@ -60,7 +60,7 @@ class CustomUser(AbstractUser):
designation = models.CharField(
max_length=100, blank=True, null=True, verbose_name=_("Designation")
)
email = EncryptedEmailField(
email = models.EmailField(
unique=True,
error_messages={
"unique": _("A user with this email already exists."),
@ -517,12 +517,12 @@ class Person(Base):
middle_name = models.CharField(
max_length=255, blank=True, null=True, verbose_name=_("Middle Name")
)
email = EncryptedEmailField(
email = models.EmailField(
unique=True,
db_index=True,
verbose_name=_("Email"),
)
phone = EncryptedCharField(
phone = models.CharField(
blank=True, null=True, verbose_name=_("Phone")
)
date_of_birth = models.DateField(
@ -538,11 +538,11 @@ class Person(Base):
gpa = models.DecimalField(
max_digits=3, decimal_places=2, verbose_name=_("GPA"),help_text=_("GPA must be between 0 and 4.")
)
national_id = EncryptedCharField(
national_id = models.CharField(
help_text=_("Enter the national id or iqama number")
)
nationality = CountryField(blank=True, null=True, verbose_name=_("Nationality"))
address = EncryptedTextField(blank=True, null=True, verbose_name=_("Address"))
address = models.TextField(blank=True, null=True, verbose_name=_("Address"))
# Optional linking to user account
user = models.OneToOneField(
@ -1790,7 +1790,7 @@ class FormSubmission(Base):
applicant_name = models.CharField(
max_length=200, blank=True, help_text="Name of the applicant"
)
applicant_email = EncryptedEmailField(
applicant_email = models.EmailField(
db_index=True, blank=True, help_text="Email of the applicant"
) # Added index
@ -2092,8 +2092,8 @@ class HiringAgency(Base):
contact_person = models.CharField(
max_length=150, blank=True, verbose_name=_("Contact Person")
)
email = EncryptedEmailField(blank=True)
phone = EncryptedCharField(max_length=20, blank=True)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True,null=True)
website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
country = CountryField(blank=True, null=True, blank_label=_("Select country"))
@ -2503,8 +2503,8 @@ class Participants(Base):
name = models.CharField(
max_length=255, verbose_name=_("Participant Name"), null=True, blank=True
)
email =EncryptedEmailField(verbose_name=_("Email"))
phone = EncryptedCharField(
email =models.EmailField(verbose_name=_("Email"))
phone = models.CharField(
max_length=12, verbose_name=_("Phone Number"), null=True, blank=True
)
designation = models.CharField(

View File

@ -459,16 +459,19 @@ def hiring_agency_created(sender, instance, created, **kwargs):
def person_created(sender, instance, created, **kwargs):
if created and not instance.user:
logger.info(f"New Person created: {instance.pk} - {instance.email}")
user = User.objects.create_user(
username=instance.email,
first_name=instance.first_name,
last_name=instance.last_name,
email=instance.email,
phone=instance.phone,
user_type="candidate",
)
instance.user = user
instance.save()
try:
user = User.objects.create_user(
username=instance.email,
first_name=instance.first_name,
last_name=instance.last_name,
email=instance.email,
phone=instance.phone,
user_type="candidate",
)
instance.user = user
instance.save()
except Exception as e:
print(e)
@receiver(post_save, sender=Source)

View File

@ -242,6 +242,7 @@ class PersonUpdateView( UpdateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin
success_url = reverse_lazy("person_list")
def form_valid(self, form):
if self.request.POST.get("view") == "portal":
form.save()
return redirect("agency_portal_persons_list")
@ -5994,9 +5995,12 @@ def application_signup(request, slug):
address = form.cleaned_data["address"]
# gpa = form.cleaned_data["gpa"]
password = form.cleaned_data["password"]
gpa=form.cleaned_data["gpa"]
natiional_id=form.cleaned_data["national_id"]
user = User.objects.create_user(
username = email,email=email,first_name=first_name,last_name=last_name,phone=phone,user_type="candidate"
username = email,email=email,first_name=first_name,last_name=last_name,phone=phone,user_type="candidate",
gpa=gpa,natiional_id=natiional_id
)
user.set_password(password)
user.save()

View File

@ -47,8 +47,7 @@ django-ckeditor-5==0.2.18
django-cors-headers==4.9.0
django-countries==7.6.1
django-crispy-forms==2.4
django-easy-audit==1.3.7
django-encrypted-model-fields==0.6.5
django-easy-audit==1.3.7s
django-extensions==4.1
django-filter==25.1
django-js-asset==3.1.2

View File

@ -252,7 +252,7 @@
</span>
{% endif %}
</div>
{% if user.is_staff %}
{% comment %} {% if user.is_staff %}
<div class="d-flex gap-2">
<a href="{% url 'person_update' person.slug %}" class="btn btn-light">
<i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %}
@ -261,7 +261,7 @@
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</a>
</div>
{% endif %}
{% endif %} {% endcomment %}
</div>
</div>
</div>

View File

@ -278,6 +278,14 @@
<label for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label>
{% include "bootstrap5/field.html" with field=form.phone %}
</div>
<div class="col-md-6">
<label for="{{ form.gpa.id_for_label }}">{{ form.gpa.label }}</label>
{% include "bootstrap5/field.html" with field=form.gpa %}
</div>
<div class="col-md-6">
<label for="{{ form.national_id.id_for_label }}">{{ form.national_id.label }}</label>
{% include "bootstrap5/field.html" with field=form.national_id %}
</div>
<div class="col-md-6">
<label for="{{ form.date_of_birth.id_for_label }}">{{ form.date_of_birth.label }}</label>
{% include "bootstrap5/field.html" with field=form.date_of_birth %}

View File

@ -326,6 +326,7 @@
<div class="modal-body" id="personModalBody">
<form id="person_form" hx-post="{% url 'person_create' %}" hx-vals='{"view":"portal","agency":"{{ agency.slug }}"}' hx-select=".persons-list" hx-target=".persons-list" hx-swap="outerHTML"
hx-on:afterRequest="$('#personModal').modal('hide')">
{% csrf_token %}
<div class="row g-4">
<div class="col-md-4">
@ -346,6 +347,14 @@
{{ person_form.phone|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
{{ person_form.gpa|as_crispy_field }}
</div>
<div class="col-md-6">
{{ person_form.national_id|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
{{ person_form.date_of_birth|as_crispy_field }}

View File

@ -106,6 +106,32 @@
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="{{ form.gpa.id_for_label }}" class="form-label">
{% trans "GPA" %} <span class="text-danger">*</span>
</label>
{{ form.gpa }}
{% if form.gpa.errors %}
<div class="text-danger small">
{{ form.gpa.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<label for="{{ form.national_id.id_for_label }}" class="form-label">
{% trans "National Id Or Iqama" %} <span class="text-danger">*</span>
</label>
{{ form.national_id }}
{% if form.national_id.errors %}
<div class="text-danger small">
{{ form.national_id.errors.0 }}
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">

View File

@ -1,68 +1,395 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% trans "Confirm Delete" %} - {{ block.super }}{% endblock %}
{% block title %}{% trans "Delete Application" %} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Main Container & Card Styling */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Warning Section */
.warning-section {
background: linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%);
border: 1px solid #ffeeba;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
text-align: center;
}
.warning-icon {
font-size: 4rem;
color: var(--kaauh-warning);
margin-bottom: 1rem;
}
.warning-title {
color: #856404;
font-weight: 700;
margin-bottom: 1rem;
}
.warning-text {
color: #856404;
margin-bottom: 0;
}
/* Info Card */
.app-info {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e9ecef;
}
.info-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.info-icon {
width: 40px;
height: 40px;
background-color: var(--kaauh-teal);
color: white;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
flex-shrink: 0;
}
.info-content {
flex: 1;
}
.info-label {
font-weight: 600;
color: var(--kaauh-primary-text);
margin-bottom: 0.25rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
color: #6c757d;
font-size: 1rem;
}
/* Button Styling */
.btn-danger {
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
font-weight: 600;
}
/* Consequence List */
.consequence-list {
list-style: none;
padding: 0;
margin: 0;
}
.consequence-list li {
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
color: #6c757d;
}
.consequence-list li:last-child {
border-bottom: none;
}
.consequence-list li i {
color: var(--kaauh-danger);
margin-right: 0.5rem;
}
/* Candidate Info Style */
.candidate-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--kaauh-teal);
}
.avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid var(--kaauh-teal);
}
/* Heroicon adjustments to match font-awesome size */
.heroicon {
width: 1.2em;
height: 1.2em;
vertical-align: -0.125em;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Delete Application" %}
</h1>
<p class="text-muted mb-0">
{% trans "You are about to delete an Application record. This action cannot be undone." %}
</p>
</div>
{# Assuming application_detail URL takes object.pk or object.slug #}
<a href="{% url 'application_detail' object.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Application" %}
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-6">
<div class="card border-danger shadow-lg">
<div class="card-header bg-danger text-white">
<h2 class="h4 mb-0 d-flex align-items-center">
<svg class="heroicon me-2" viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 11V17"></path>
<path d="M14 11V17"></path>
<path d="M4 7H20"></path>
<path d="M6 7H18V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H8C7.46957 21 6.96086 20.7893 6.58579 20.4142C6.21071 20.0391 6 19.5304 6 19V7Z"></path>
<path d="M9 7V4C9 3.46957 9.21071 2.96086 9.58579 2.58579C9.96086 2.21071 10.4696 2 11 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V7"></path>
</svg>
{% trans "Confirm Deletion" %}
</h2>
<div class="col-lg-8">
<div class="warning-section">
<div class="warning-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3>
<p class="warning-text">
{% blocktrans with candidate_name=object.candidate.full_name job_title=object.job.title %}
Deleting the application submitted by **{{ candidate_name }}** for the job **{{ job_title }}** will permanently remove all associated data. Please review the information below carefully before proceeding.
{% endblocktrans %}
</p>
</div>
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-file-alt me-2"></i>
{% trans "Application to be Deleted" %}
</h5>
</div>
<div class="card-body">
<p class="lead text-danger fw-bold">
{% trans "You are about to permanently delete the following Application." %}
</p>
<p>
{% blocktrans with candidate_name=object.candidate.full_name job_title=object.job.title %}
**Are you absolutely sure** you want to delete the application submitted by **{{ object}}** ?
{% endblocktrans %}
</p>
<blockquote class="blockquote border-start border-danger border-4 ps-3 py-2 bg-light rounded-end">
<h5 class="mb-1 text-dark">{% trans "Application Details" %}</h5>
<div class="app-info">
{% if object.candidate %}
<p class="mb-0"><strong>{% trans "Candidate:" %}</strong> {{ object.candidate.full_name }}</p>
<div class="d-flex align-items-center mb-4">
{# Assuming candidate has a profile_image field #}
{% if object.candidate.profile_image %}
<img src="{{ object.candidate.profile_image.url }}" alt="{{ object.candidate.full_name }}" class="candidate-avatar me-3">
{% else %}
<div class="avatar-placeholder me-3">
<i class="fas fa-user text-muted fa-2x"></i>
</div>
{% endif %}
<div>
<h4 class="mb-1">{{ object.candidate.full_name }}</h4>
{% if object.candidate.email %}
<p class="text-muted mb-0">{{ object.candidate.email }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% if object.job %}
<p class="mb-0"><strong>{% trans "Job Title:" %}</strong> {{ object.job.title }}</p>
{% endif %}
<p class="mb-0"><strong>{% trans "Applied On:" %}</strong> {{ object.created_at|date:"M d, Y \a\t P" }}</p>
</blockquote>
<p class="mt-4 text-muted">
{% trans "This action is **irreversible** and all associated data will be lost." %}
</p>
{% if object.job %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-briefcase"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Job Title" %}</div>
<div class="info-value">{{ object.job.title }}</div>
</div>
</div>
{% endif %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Applied On" %}</div>
<div class="info-value">{{ object.created_at|date:"F d, Y \a\t P" }}</div>
</div>
</div>
{% if object.status %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-cogs"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Current Status" %}</div>
<div class="info-value">{{ object.get_status_display }}</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<a href="{% url 'application_list' %}" class="btn btn-secondary">
{% trans "Cancel" %}
</a>
<form method="post">
</div>
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-list me-2"></i>
{% trans "What will happen when you delete this Application?" %}
</h5>
</div>
<div class="card-body">
<ul class="consequence-list">
<li>
<i class="fas fa-times-circle"></i>
{% trans "The Application record and all status history will be permanently deleted." %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "All associated screening results, scores, and evaluations will be removed." %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "Any linked documents (CV, cover letter) specific to this application will be lost." %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "The Candidate will still exist, but this specific application history will be lost." %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "This action cannot be undone under any circumstances." %}
</li>
</ul>
</div>
</div>
<div class="card kaauh-card">
<div class="card-body">
<form method="post" id="deleteForm">
{% csrf_token %}
<button type="submit" class="btn btn-danger d-flex align-items-center">
<svg class="heroicon me-1" viewBox="0 0 24 24" width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
{% trans "Yes, Permanently Delete" %}
</button>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirm_delete" name="confirm_delete" required>
<label class="form-check-label" for="confirm_delete">
<strong>{% trans "I understand that this action cannot be undone and I want to permanently delete this application." %}</strong>
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'application_detail' object.pk %}" class="btn btn-secondary btn-lg">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="btn btn-danger btn-lg"
id="deleteButton"
disabled>
<i class="fas fa-trash me-2"></i>
{% trans "Delete Application Permanently" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const confirmDeleteCheckbox = document.getElementById('confirm_delete');
const deleteButton = document.getElementById('deleteButton');
const deleteForm = document.getElementById('deleteForm');
function validateForm() {
const checkboxChecked = confirmDeleteCheckbox.checked;
deleteButton.disabled = !checkboxChecked;
// Toggle button classes for visual feedback
if (checkboxChecked) {
deleteButton.classList.remove('btn-secondary');
deleteButton.classList.add('btn-danger');
} else {
deleteButton.classList.remove('btn-danger');
deleteButton.classList.add('btn-secondary');
}
}
confirmDeleteCheckbox.addEventListener('change', validateForm);
// Initialize state on page load
validateForm();
// Add confirmation and prevent double submission before final submission
deleteForm.addEventListener('submit', function(event) {
event.preventDefault();
const candidateName = "{{ object.candidate.full_name|escapejs }}";
const jobTitle = "{{ object.job.title|escapejs }}";
// Construct a confirmation message using string replacement for safety
const confirmationMessageTemplate = "{% blocktrans with candidate_name='CANDIDATE_PLACEHOLDER' job_title='JOB_PLACEHOLDER' %}Are you absolutely sure you want to permanently delete the application by CANDIDATE_PLACEHOLDER for JOB_PLACEHOLDER? This action cannot be reversed!{% endblocktrans %}";
const confirmationMessage = confirmationMessageTemplate
.replace('CANDIDATE_PLACEHOLDER', candidateName)
.replace('JOB_PLACEHOLDER', jobTitle);
if (confirm(confirmationMessage)) {
// Disable button and show loading state
deleteButton.disabled = true;
deleteButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>{% trans "Deleting..." %}';
// Submit the form programmatically
deleteForm.submit();
} else {
// If the user cancels the dialog, ensure the button state is reset/validated
validateForm();
}
});
});
</script>
{% endblock %}