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", "django_q",
"widget_tweaks", "widget_tweaks",
"easyaudit", "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'}), "gpa": forms.TextInput(attrs={'class': 'custom-decimal-input'}),
"national_id":forms.NumberInput(attrs={'min': 0, 'step': 1}), "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 ApplicationForm(forms.ModelForm):
class Meta: class Meta:
@ -987,7 +1023,7 @@ class HiringAgencyForm(forms.ModelForm):
} }
), ),
"email": forms.EmailInput( "email": forms.EmailInput(
attrs={"class": "form-control"} attrs={"class": "form-control","required": True}
), ),
"phone": forms.TextInput( "phone": forms.TextInput(
attrs={"class": "form-control"} attrs={"class": "form-control"}
@ -1076,6 +1112,7 @@ class HiringAgencyForm(forms.ModelForm):
# instance = self.instance # instance = self.instance
email = email.lower().strip() email = email.lower().strip()
if not instance.pk: # Creating new instance if not instance.pk: # Creating new instance
print("created ....")
if HiringAgency.objects.filter(email=email).exists(): if HiringAgency.objects.filter(email=email).exists():
raise ValidationError("An agency with this email already exists.") raise ValidationError("An agency with this email already exists.")
else: # Editing existing instance 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.models
import django.contrib.auth.validators import django.contrib.auth.validators
@ -8,7 +8,6 @@ import django.utils.timezone
import django_ckeditor_5.fields import django_ckeditor_5.fields
import django_countries.fields import django_countries.fields
import django_extensions.db.fields import django_extensions.db.fields
import encrypted_model_fields.fields
import recruitment.validators import recruitment.validators
from django.conf import settings from django.conf import settings
from django.db import migrations, models 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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)), ('email', models.EmailField(max_length=254, unique=True)),
('phone', models.CharField(blank=True, max_length=20)), ('phone', models.CharField(blank=True, max_length=20, null=True)),
('website', models.URLField(blank=True)), ('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')), ('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)), ('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')), ('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last 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')), ('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')), ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
('phone', encrypted_model_fields.fields.EncryptedPositiveIntegerField(blank=True, null=True, verbose_name='Phone')), ('phone', models.CharField(blank=True, null=True, verbose_name='Phone')),
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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 import F, Value, IntegerField, CharField,Q
from django.db.models.functions import Coalesce, Cast from django.db.models.functions import Coalesce, Cast
from django.db.models.fields.json import KeyTransform, KeyTextTransform from django.db.models.fields.json import KeyTransform, KeyTextTransform
from encrypted_model_fields.fields import EncryptedCharField,EncryptedEmailField,EncryptedTextField
class EmailContent(models.Model): class EmailContent(models.Model):
subject = models.CharField(max_length=255, verbose_name=_("Subject")) subject = models.CharField(max_length=255, verbose_name=_("Subject"))
@ -47,7 +47,7 @@ class CustomUser(AbstractUser):
user_type = models.CharField( user_type = models.CharField(
max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type") max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type")
) )
phone = EncryptedCharField( phone = models.CharField(
blank=True, null=True, verbose_name=_("Phone") blank=True, null=True, verbose_name=_("Phone")
) )
profile_image = models.ImageField( profile_image = models.ImageField(
@ -60,7 +60,7 @@ 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 = EncryptedEmailField( email = models.EmailField(
unique=True, unique=True,
error_messages={ error_messages={
"unique": _("A user with this email already exists."), "unique": _("A user with this email already exists."),
@ -517,12 +517,12 @@ class Person(Base):
middle_name = models.CharField( middle_name = models.CharField(
max_length=255, blank=True, null=True, verbose_name=_("Middle Name") max_length=255, blank=True, null=True, verbose_name=_("Middle Name")
) )
email = EncryptedEmailField( email = models.EmailField(
unique=True, unique=True,
db_index=True, db_index=True,
verbose_name=_("Email"), verbose_name=_("Email"),
) )
phone = EncryptedCharField( phone = models.CharField(
blank=True, null=True, verbose_name=_("Phone") blank=True, null=True, verbose_name=_("Phone")
) )
date_of_birth = models.DateField( date_of_birth = models.DateField(
@ -538,11 +538,11 @@ class Person(Base):
gpa = models.DecimalField( gpa = models.DecimalField(
max_digits=3, decimal_places=2, verbose_name=_("GPA"),help_text=_("GPA must be between 0 and 4.") 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") help_text=_("Enter the national id or iqama number")
) )
nationality = CountryField(blank=True, null=True, verbose_name=_("Nationality")) 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 # Optional linking to user account
user = models.OneToOneField( user = models.OneToOneField(
@ -1790,7 +1790,7 @@ class FormSubmission(Base):
applicant_name = models.CharField( applicant_name = models.CharField(
max_length=200, blank=True, help_text="Name of the applicant" 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" db_index=True, blank=True, help_text="Email of the applicant"
) # Added index ) # Added index
@ -2092,8 +2092,8 @@ class HiringAgency(Base):
contact_person = models.CharField( contact_person = models.CharField(
max_length=150, blank=True, verbose_name=_("Contact Person") max_length=150, blank=True, verbose_name=_("Contact Person")
) )
email = EncryptedEmailField(blank=True) email = models.EmailField(unique=True)
phone = EncryptedCharField(max_length=20, blank=True) phone = models.CharField(max_length=20, blank=True,null=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency")) notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
country = CountryField(blank=True, null=True, blank_label=_("Select country")) country = CountryField(blank=True, null=True, blank_label=_("Select country"))
@ -2503,8 +2503,8 @@ class Participants(Base):
name = models.CharField( name = models.CharField(
max_length=255, verbose_name=_("Participant Name"), null=True, blank=True max_length=255, verbose_name=_("Participant Name"), null=True, blank=True
) )
email =EncryptedEmailField(verbose_name=_("Email")) email =models.EmailField(verbose_name=_("Email"))
phone = EncryptedCharField( phone = models.CharField(
max_length=12, verbose_name=_("Phone Number"), null=True, blank=True max_length=12, verbose_name=_("Phone Number"), null=True, blank=True
) )
designation = models.CharField( designation = models.CharField(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -326,6 +326,7 @@
<div class="modal-body" id="personModalBody"> <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" <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')"> hx-on:afterRequest="$('#personModal').modal('hide')">
{% csrf_token %} {% csrf_token %}
<div class="row g-4"> <div class="row g-4">
<div class="col-md-4"> <div class="col-md-4">
@ -346,6 +347,14 @@
{{ person_form.phone|as_crispy_field }} {{ person_form.phone|as_crispy_field }}
</div> </div>
</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="row g-4">
<div class="col-md-6"> <div class="col-md-6">
{{ person_form.date_of_birth|as_crispy_field }} {{ person_form.date_of_birth|as_crispy_field }}

View File

@ -106,6 +106,32 @@
</div> </div>
</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="row">
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label"> <label for="{{ form.phone.id_for_label }}" class="form-label">

View File

@ -1,68 +1,395 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% 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 %} {% 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="row justify-content-center">
<div class="col-lg-8 col-xl-6"> <div class="col-lg-8">
<div class="card border-danger shadow-lg"> <div class="warning-section">
<div class="card-header bg-danger text-white"> <div class="warning-icon">
<h2 class="h4 mb-0 d-flex align-items-center"> <i class="fas fa-exclamation-triangle"></i>
<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"> </div>
<path d="M10 11V17"></path> <h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3>
<path d="M14 11V17"></path> <p class="warning-text">
<path d="M4 7H20"></path> {% blocktrans with candidate_name=object.candidate.full_name job_title=object.job.title %}
<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> 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.
<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> {% endblocktrans %}
</svg> </p>
{% trans "Confirm Deletion" %} </div>
</h2>
<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>
<div class="card-body"> <div class="card-body">
<p class="lead text-danger fw-bold"> <div class="app-info">
{% 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>
{% if object.candidate %} {% 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 %} {% 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"> {% if object.job %}
{% trans "This action is **irreversible** and all associated data will be lost." %} <div class="info-item">
</p> <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>
<div class="card-footer d-flex justify-content-between align-items-center"> </div>
<a href="{% url 'application_list' %}" class="btn btn-secondary">
{% trans "Cancel" %} <div class="card kaauh-card mb-4">
</a> <div class="card-header bg-white border-bottom">
<form method="post"> <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 %} {% 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"> <div class="mb-4">
<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> <div class="form-check">
</svg> <input class="form-check-input" type="checkbox" id="confirm_delete" name="confirm_delete" required>
{% trans "Yes, Permanently Delete" %} <label class="form-check-label" for="confirm_delete">
</button> <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> </form>
</div> </div>
</div> </div>
</div> </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 %} {% endblock %}