add delete
This commit is contained in:
parent
1fad9839af
commit
a96e618a6a
@ -295,10 +295,10 @@ class JobPosting(Base):
|
||||
next_num = 1
|
||||
|
||||
self.internal_job_id = f"{prefix}-{year}-{next_num:06d}"
|
||||
|
||||
|
||||
if self.department:
|
||||
self.department = self.department.title()
|
||||
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_location_display(self):
|
||||
|
||||
335
requirements.txt
335
requirements.txt
@ -1,146 +1,191 @@
|
||||
annotated-types
|
||||
appdirs
|
||||
asgiref
|
||||
asteval
|
||||
astunparse
|
||||
attrs
|
||||
blinker
|
||||
blis
|
||||
boto3
|
||||
botocore
|
||||
bw-migrations
|
||||
bw2parameters
|
||||
bw_processing
|
||||
cached-property
|
||||
catalogue
|
||||
certifi
|
||||
channels
|
||||
chardet
|
||||
charset-normalizer
|
||||
click
|
||||
cloudpathlib
|
||||
confection
|
||||
constructive_geometries
|
||||
country_converter
|
||||
cymem
|
||||
dataflows-tabulator
|
||||
datapackage
|
||||
deepdiff
|
||||
Deprecated
|
||||
Django
|
||||
django-allauth
|
||||
django-cors-headers
|
||||
django-filter
|
||||
django-unfold
|
||||
djangorestframework
|
||||
docopt
|
||||
amqp==5.3.1
|
||||
annotated-types==0.7.0
|
||||
anthropic==0.63.0
|
||||
anyio==4.11.0
|
||||
appdirs==1.4.4
|
||||
arrow==1.3.0
|
||||
asgiref==3.9.2
|
||||
asteval==1.0.6
|
||||
astunparse==1.6.3
|
||||
attrs==25.3.0
|
||||
billiard==4.2.2
|
||||
bleach==6.2.0
|
||||
blessed==1.22.0
|
||||
blinker==1.9.0
|
||||
blis==1.3.0
|
||||
boto3==1.40.37
|
||||
botocore==1.40.37
|
||||
bw-migrations==0.2
|
||||
bw2data==4.5
|
||||
bw2parameters==1.1.0
|
||||
bw_processing==1.0
|
||||
cached-property==2.0.1
|
||||
catalogue==2.0.10
|
||||
celery==5.5.3
|
||||
certifi==2025.8.3
|
||||
channels==4.3.1
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.3
|
||||
click==8.3.0
|
||||
click-didyoumean==0.3.1
|
||||
click-plugins==1.1.1.2
|
||||
click-repl==0.3.0
|
||||
cloudpathlib==0.22.0
|
||||
confection==0.1.5
|
||||
constructive_geometries==1.0
|
||||
country_converter==1.3.1
|
||||
crispy-bootstrap5==2025.6
|
||||
cymem==2.0.11
|
||||
dataflows-tabulator==1.54.3
|
||||
datapackage==1.15.4
|
||||
datastar-py==0.6.5
|
||||
deepdiff==7.0.1
|
||||
Deprecated==1.2.18
|
||||
distro==1.9.0
|
||||
Django==5.2.6
|
||||
django-allauth==65.11.2
|
||||
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-extensions==4.1
|
||||
django-filter==25.1
|
||||
django-picklefield==3.3
|
||||
django-q2==1.8.0
|
||||
django-summernote==0.8.20.0
|
||||
django-template-partials==25.2
|
||||
django-unfold==0.66.0
|
||||
django-widget-tweaks==1.5.0
|
||||
django_celery_results==2.6.0
|
||||
djangorestframework==3.16.1
|
||||
docopt==0.6.2
|
||||
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
|
||||
et_xmlfile
|
||||
Faker
|
||||
flexcache
|
||||
flexparser
|
||||
fsspec
|
||||
idna
|
||||
ijson
|
||||
isodate
|
||||
Jinja2
|
||||
jmespath
|
||||
jsonlines
|
||||
jsonpointer
|
||||
jsonschema
|
||||
jsonschema-specifications
|
||||
langcodes
|
||||
language_data
|
||||
linear-tsv
|
||||
llvmlite
|
||||
loguru
|
||||
lxml
|
||||
marisa-trie
|
||||
markdown-it-py
|
||||
MarkupSafe
|
||||
matrix_utils
|
||||
mdurl
|
||||
morefs
|
||||
mrio-common-metadata
|
||||
murmurhash
|
||||
numba
|
||||
numpy
|
||||
openpyxl
|
||||
ordered-set
|
||||
packaging
|
||||
pandas
|
||||
peewee
|
||||
Pint
|
||||
platformdirs
|
||||
preshed
|
||||
prettytable
|
||||
pydantic
|
||||
pydantic-settings
|
||||
pydantic_core
|
||||
pyecospold
|
||||
Pygments
|
||||
PyJWT
|
||||
PyMuPDF
|
||||
pyparsing
|
||||
PyPrind
|
||||
python-dateutil
|
||||
python-dotenv
|
||||
python-json-logger
|
||||
pytz
|
||||
pyxlsb
|
||||
PyYAML
|
||||
randonneur
|
||||
randonneur_data
|
||||
RapidFuzz
|
||||
rdflib
|
||||
referencing
|
||||
requests
|
||||
rfc3986
|
||||
rich
|
||||
rpds-py
|
||||
s3transfer
|
||||
scipy
|
||||
shellingham
|
||||
six
|
||||
smart-open
|
||||
snowflake-id
|
||||
spacy
|
||||
spacy-legacy
|
||||
spacy-loggers
|
||||
SPARQLWrapper
|
||||
sparse
|
||||
SQLAlchemy
|
||||
sqlparse
|
||||
srsly
|
||||
stats_arrays
|
||||
structlog
|
||||
tableschema
|
||||
thinc
|
||||
toolz
|
||||
tqdm
|
||||
typer
|
||||
typing-inspection
|
||||
typing_extensions
|
||||
tzdata
|
||||
unicodecsv
|
||||
urllib3
|
||||
voluptuous
|
||||
wasabi
|
||||
wcwidth
|
||||
weasel
|
||||
wrapt
|
||||
wurst
|
||||
xlrd
|
||||
XlsxWriter
|
||||
celery[redis]
|
||||
redis
|
||||
sentence-transformers
|
||||
torch
|
||||
pdfplumber
|
||||
python-docx
|
||||
PyMuPDF
|
||||
pytesseract
|
||||
Pillow
|
||||
python-dotenv
|
||||
django-countries
|
||||
django-q2
|
||||
et_xmlfile==2.0.0
|
||||
Faker==37.8.0
|
||||
flexcache==0.3
|
||||
flexparser==0.4
|
||||
fsspec==2025.9.0
|
||||
gpt-po-translator==1.3.2
|
||||
greenlet==3.2.4
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
ijson==3.4.0
|
||||
iniconfig==2.1.0
|
||||
isodate==0.7.2
|
||||
isort==5.13.2
|
||||
Jinja2==3.1.6
|
||||
jiter==0.11.1
|
||||
jmespath==1.0.1
|
||||
jsonlines==4.0.0
|
||||
jsonpointer==3.0.0
|
||||
jsonschema==4.25.1
|
||||
jsonschema-specifications==2025.9.1
|
||||
kombu==5.5.4
|
||||
langcodes==3.5.0
|
||||
language_data==1.3.0
|
||||
linear-tsv==1.1.0
|
||||
llvmlite==0.45.0
|
||||
loguru==0.7.3
|
||||
lxml==6.0.2
|
||||
marisa-trie==1.3.1
|
||||
markdown-it-py==4.0.0
|
||||
MarkupSafe==3.0.2
|
||||
matrix_utils==0.6.2
|
||||
mdurl==0.1.2
|
||||
morefs==0.2.2
|
||||
mrio-common-metadata==0.2.1
|
||||
murmurhash==1.0.13
|
||||
numba==0.62.0
|
||||
numpy==2.3.3
|
||||
openai==1.99.9
|
||||
openpyxl==3.1.5
|
||||
ordered-set==4.1.0
|
||||
packaging==25.0
|
||||
pandas==2.3.2
|
||||
peewee==3.18.2
|
||||
pillow==11.3.0
|
||||
Pint==0.25
|
||||
platformdirs==4.4.0
|
||||
pluggy==1.6.0
|
||||
polib==1.2.0
|
||||
preshed==3.0.10
|
||||
prettytable==3.16.0
|
||||
prompt_toolkit==3.0.52
|
||||
psycopg2-binary==2.9.11
|
||||
pycountry==24.6.1
|
||||
pydantic==2.11.9
|
||||
pydantic-settings==2.10.1
|
||||
pydantic_core==2.33.2
|
||||
pyecospold==4.0.0
|
||||
Pygments==2.19.2
|
||||
PyJWT==2.10.1
|
||||
PyMuPDF==1.26.4
|
||||
pyparsing==3.2.5
|
||||
PyPDF2==3.0.1
|
||||
PyPrind==2.11.3
|
||||
pytest==8.3.4
|
||||
pytest-django==4.11.1
|
||||
python-dateutil==2.9.0.post0
|
||||
python-docx==1.2.0
|
||||
python-dotenv==1.0.1
|
||||
python-json-logger==3.3.0
|
||||
pytz==2025.2
|
||||
pyxlsb==1.0.10
|
||||
PyYAML==6.0.2
|
||||
randonneur==0.6.2
|
||||
randonneur_data==0.6
|
||||
RapidFuzz==3.14.1
|
||||
rdflib==7.2.1
|
||||
redis==3.5.3
|
||||
referencing==0.36.2
|
||||
requests==2.32.3
|
||||
responses==0.25.8
|
||||
rfc3986==2.0.0
|
||||
rich==14.1.0
|
||||
rpds-py==0.27.1
|
||||
s3transfer==0.14.0
|
||||
scipy==1.16.2
|
||||
setuptools==80.9.0
|
||||
setuptools-scm==8.1.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
smart_open==7.3.1
|
||||
sniffio==1.3.1
|
||||
snowflake-id==1.0.2
|
||||
spacy==3.8.7
|
||||
spacy-legacy==3.0.12
|
||||
spacy-loggers==1.0.5
|
||||
SPARQLWrapper==2.0.0
|
||||
sparse==0.17.0
|
||||
SQLAlchemy==2.0.43
|
||||
sqlparse==0.5.3
|
||||
srsly==2.5.1
|
||||
stats_arrays==0.7
|
||||
structlog==25.4.0
|
||||
tableschema==1.21.0
|
||||
tenacity==9.0.0
|
||||
thinc==8.3.6
|
||||
tomli==2.2.1
|
||||
toolz==1.0.0
|
||||
tqdm==4.67.1
|
||||
typer==0.19.2
|
||||
types-python-dateutil==2.9.0.20251008
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.2
|
||||
unicodecsv==0.14.1
|
||||
urllib3==2.5.0
|
||||
vine==5.1.0
|
||||
voluptuous==0.15.2
|
||||
wasabi==1.1.3
|
||||
wcwidth==0.2.14
|
||||
weasel==0.4.1
|
||||
webencodings==0.5.1
|
||||
wheel==0.45.1
|
||||
wrapt==1.17.3
|
||||
wurst==0.4
|
||||
xlrd==2.0.2
|
||||
xlsxwriter==3.2.9
|
||||
|
||||
372
templates/people/delete_person.html
Normal file
372
templates/people/delete_person.html
Normal file
@ -0,0 +1,372 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Person" %} - {{ 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;
|
||||
}
|
||||
|
||||
/* Person Info Card */
|
||||
.person-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;
|
||||
}
|
||||
|
||||
/* Person Profile Image */
|
||||
.person-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);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Header Section -->
|
||||
<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 Person" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "You are about to delete a person record. This action cannot be undone." %}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{% url 'person_detail' object.slug %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Person" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<!-- Warning Section -->
|
||||
<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">
|
||||
{% trans "Deleting this person will permanently remove all associated data. Please review the information below carefully before proceeding." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Person Information -->
|
||||
<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-user me-2"></i>
|
||||
{% trans "Person to be Deleted" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="person-info">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
{% if object.profile_image %}
|
||||
<img src="{{ object.profile_image.url }}" alt="{{ object.get_full_name }}" class="person-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.get_full_name }}</h4>
|
||||
{% if object.email %}
|
||||
<p class="text-muted mb-0">{{ object.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.phone %}
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-phone"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Phone" %}</div>
|
||||
<div class="info-value">{{ object.phone }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-calendar"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Created On" %}</div>
|
||||
<div class="info-value">{{ object.created_at|date:"F d, Y" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.nationality %}
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-globe"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Nationality" %}</div>
|
||||
<div class="info-value">{{ object.nationality }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if object.gender %}
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-venus-mars"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Gender" %}</div>
|
||||
<div class="info-value">{{ object.get_gender_display }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consequences -->
|
||||
<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 person?" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="consequence-list">
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "The person profile and all personal information will be permanently deleted" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "All associated applications and documents will be removed" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "Any interview schedules and history will be deleted" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "All related data and records 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>
|
||||
|
||||
<!-- Confirmation Form -->
|
||||
<div class="card kaauh-card">
|
||||
<div class="card-body">
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<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 person." %}</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'person_detail' object.slug %}" 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 Person 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;
|
||||
|
||||
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);
|
||||
|
||||
// Add confirmation before final submission
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
60
templates/people/person_confirm_delete.html
Normal file
60
templates/people/person_confirm_delete.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Person" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Confirm Deletion" %}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Warning: This action cannot be undone!" %}
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
{% trans "You are about to permanently delete this person and all associated data." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
{% if person.profile_image %}
|
||||
<img src="{{ person.profile_image.url }}" alt="{{ person.get_full_name }}"
|
||||
class="rounded-circle mb-3" style="width: 100px; height: 100px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-3"
|
||||
style="width: 100px; height: 100px;">
|
||||
<i class="fas fa-user text-muted fa-2x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h5>{{ person.get_full_name }}</h5>
|
||||
{% if person.email %}
|
||||
<p class="text-muted mb-0">{{ person.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'person_update' person.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete Person" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -163,7 +163,7 @@
|
||||
<div class="card mb-4 shadow-sm no-hover">
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
|
||||
<div class="input-group input-group-lg">
|
||||
@ -213,8 +213,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{% if people_list %}
|
||||
<div id="person-list">
|
||||
<!-- View Switcher -->
|
||||
@ -287,13 +287,13 @@
|
||||
class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
{% comment %} <button type="button" class="btn btn-outline-danger"
|
||||
title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'person_delete' person.slug %}"
|
||||
data-item-name="{{ person.get_full_name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</button> {% endcomment %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -194,6 +194,9 @@
|
||||
<a href="{% url 'person_detail' person.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
<a href="{% url 'person_delete' person.slug %}" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
|
||||
@ -1,217 +1,478 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n widget_tweaks %}
|
||||
|
||||
{% block title %}{{ title }} - ATS{% endblock %}
|
||||
{% block title %}{{ title }} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* UI Variables for the KAAT-S Theme */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Form Container Styling */
|
||||
.form-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Card Styling */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Secondary Button Style */
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Form Field Styling */
|
||||
.form-control:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
|
||||
/* Breadcrumb Styling */
|
||||
.breadcrumb {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: ">";
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Alert Styling */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: auto;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Current Profile Section */
|
||||
.current-profile {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.current-profile h6 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">{{ title }}</h1>
|
||||
<p class="text-muted mb-0">
|
||||
<div class="container-fluid py-4">
|
||||
<div class="form-container">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'agency_list' %}" class="text-decoration-none text-secondary">
|
||||
<i class="fas fa-building me-1"></i> {% trans "Agencies" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if agency %}
|
||||
{% trans "Update the hiring agency information below." %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'agency_detail' agency.slug %}" class="text-decoration-none text-secondary">
|
||||
{{ agency.name }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page"
|
||||
style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;">{% trans "Update" %}</li>
|
||||
{% else %}
|
||||
{% trans "Fill in the details to add a new hiring agency." %}
|
||||
<li class="breadcrumb-item active" aria-current="page"
|
||||
style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;">{% trans "Create" %}</li>
|
||||
{% endif %}
|
||||
</p>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-building me-2"></i> {{ title }}
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
{% if agency %}
|
||||
<a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
<a href="{% url 'agency_delete' agency.slug %}" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'agency_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'agency_list' %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Agencies" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Please correct the errors below:" %}
|
||||
</h5>
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="mb-0">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% if agency %}
|
||||
<!-- Current Agency Info -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="current-profile">
|
||||
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="current-image d-flex align-items-center justify-content-center bg-light">
|
||||
<i class="fas fa-building text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Name -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
{% for error in form.name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
<div>
|
||||
<h5 class="mb-1">{{ agency.name }}</h5>
|
||||
{% if agency.contact_person %}
|
||||
<p class="text-muted mb-0">{% trans "Contact" %}: {{ agency.contact_person }}</p>
|
||||
{% endif %}
|
||||
{% if form.name.help_text %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
{% if agency.email %}
|
||||
<p class="text-muted mb-0">{{ agency.email }}</p>
|
||||
{% endif %}
|
||||
<small class="text-muted">
|
||||
{% trans "Created" %}: {{ agency.created_at|date:"d M Y" }} •
|
||||
{% trans "Last Updated" %}: {{ agency.updated_at|date:"d M Y" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Contact Person and Phone -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.contact_person.id_for_label }}" class="form-label">
|
||||
{{ form.contact_person.label }}
|
||||
</label>
|
||||
{{ form.contact_person }}
|
||||
{% if form.contact_person.errors %}
|
||||
{% for error in form.contact_person.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.contact_person.help_text %}
|
||||
<div class="form-text">{{ form.contact_person.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.phone.id_for_label }}" class="form-label">
|
||||
{{ form.phone.label }}
|
||||
</label>
|
||||
{{ form.phone }}
|
||||
{% if form.phone.errors %}
|
||||
{% for error in form.phone.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.phone.help_text %}
|
||||
<div class="form-text">{{ form.phone.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email and Website -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.email.id_for_label }}" class="form-label">
|
||||
{{ form.email.label }}
|
||||
</label>
|
||||
{{ form.email }}
|
||||
{% if form.email.errors %}
|
||||
{% for error in form.email.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.email.help_text %}
|
||||
<div class="form-text">{{ form.email.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.website.id_for_label }}" class="form-label">
|
||||
{{ form.website.label }}
|
||||
</label>
|
||||
{{ form.website }}
|
||||
{% if form.website.errors %}
|
||||
{% for error in form.website.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.website.help_text %}
|
||||
<div class="form-text">{{ form.website.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.address.id_for_label }}" class="form-label">
|
||||
{{ form.address.label }}
|
||||
</label>
|
||||
{{ form.address }}
|
||||
{% if form.address.errors %}
|
||||
{% for error in form.address.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.address.help_text %}
|
||||
<div class="form-text">{{ form.address.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Country and City -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.country.id_for_label }}" class="form-label">
|
||||
{{ form.country.label }}
|
||||
</label>
|
||||
{{ form.country }}
|
||||
{% if form.country.errors %}
|
||||
{% for error in form.country.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.country.help_text %}
|
||||
<div class="form-text">{{ form.country.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.city.id_for_label }}" class="form-label">
|
||||
{{ form.city.label }}
|
||||
</label>
|
||||
{{ form.city }}
|
||||
{% if form.city.errors %}
|
||||
{% for error in form.city.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.city.help_text %}
|
||||
<div class="form-text">{{ form.city.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
{% for error in form.description.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.description.help_text %}
|
||||
<div class="form-text">{{ form.description.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'agency_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {{ button_text }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
|
||||
</h5>
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="mb-0">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate id="agency-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Name -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.name|add_class:"form-control" }}
|
||||
{% if form.name.errors %}
|
||||
{% for error in form.name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.name.help_text %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Contact Person and Phone -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.contact_person.id_for_label }}" class="form-label">
|
||||
{{ form.contact_person.label }}
|
||||
</label>
|
||||
{{ form.contact_person|add_class:"form-control" }}
|
||||
{% if form.contact_person.errors %}
|
||||
{% for error in form.contact_person.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.contact_person.help_text %}
|
||||
<div class="form-text">{{ form.contact_person.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.phone.id_for_label }}" class="form-label">
|
||||
{{ form.phone.label }}
|
||||
</label>
|
||||
{{ form.phone|add_class:"form-control" }}
|
||||
{% if form.phone.errors %}
|
||||
{% for error in form.phone.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.phone.help_text %}
|
||||
<div class="form-text">{{ form.phone.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email and Website -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.email.id_for_label }}" class="form-label">
|
||||
{{ form.email.label }}
|
||||
</label>
|
||||
{{ form.email|add_class:"form-control" }}
|
||||
{% if form.email.errors %}
|
||||
{% for error in form.email.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.email.help_text %}
|
||||
<div class="form-text">{{ form.email.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.website.id_for_label }}" class="form-label">
|
||||
{{ form.website.label }}
|
||||
</label>
|
||||
{{ form.website|add_class:"form-control" }}
|
||||
{% if form.website.errors %}
|
||||
{% for error in form.website.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.website.help_text %}
|
||||
<div class="form-text">{{ form.website.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.address.id_for_label }}" class="form-label">
|
||||
{{ form.address.label }}
|
||||
</label>
|
||||
{{ form.address|add_class:"form-control" }}
|
||||
{% if form.address.errors %}
|
||||
{% for error in form.address.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.address.help_text %}
|
||||
<div class="form-text">{{ form.address.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Country and City -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.country.id_for_label }}" class="form-label">
|
||||
{{ form.country.label }}
|
||||
</label>
|
||||
{{ form.country|add_class:"form-control" }}
|
||||
{% if form.country.errors %}
|
||||
{% for error in form.country.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.country.help_text %}
|
||||
<div class="form-text">{{ form.country.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.city.id_for_label }}" class="form-label">
|
||||
{{ form.city.label }}
|
||||
</label>
|
||||
{{ form.city|add_class:"form-control" }}
|
||||
{% if form.city.errors %}
|
||||
{% for error in form.city.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.city.help_text %}
|
||||
<div class="form-text">{{ form.city.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
{{ form.description|add_class:"form-control" }}
|
||||
{% if form.description.errors %}
|
||||
{% for error in form.description.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if form.description.help_text %}
|
||||
<div class="form-text">{{ form.description.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button form="agency-form" type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {{ button_text }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add Bootstrap classes to form fields
|
||||
const formFields = document.querySelectorAll('input[type="text"], input[type="email"], input[type="url"], input[type="tel"], textarea, select');
|
||||
formFields.forEach(function(field) {
|
||||
field.classList.add('form-control');
|
||||
// Form Validation
|
||||
const form = document.getElementById('agency-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Basic validation
|
||||
const name = document.getElementById('id_name');
|
||||
if (name && !name.value.trim()) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Agency name is required." %}');
|
||||
return;
|
||||
}
|
||||
|
||||
const email = document.getElementById('id_email');
|
||||
if (email && email.value.trim() && !isValidEmail(email.value.trim())) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Please enter a valid email address." %}');
|
||||
return;
|
||||
}
|
||||
|
||||
const website = document.getElementById('id_website');
|
||||
if (website && website.value.trim() && !isValidURL(website.value.trim())) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Please enter a valid website URL." %}');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Email validation helper
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// URL validation helper
|
||||
function isValidURL(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Warn before leaving if changes are made
|
||||
let formChanged = false;
|
||||
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
|
||||
|
||||
formInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (formChanged) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
formChanged = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,43 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
|
||||
{% block title %}Update Candidate - {{ block.super }}{% endblock %}
|
||||
{% block title %}Update {{ object.name }} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES (FROM JOB DETAIL) */
|
||||
/* ================================================= */
|
||||
/* UI Variables for the KAAT-S Theme */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
/* Form Container Styling */
|
||||
.form-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Card Styling */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action, .btn-primary {
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
.btn-main-action:hover, .btn-primary:hover {
|
||||
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Outlined Button Styles */
|
||||
|
||||
/* Secondary Button Style */
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
@ -48,94 +56,308 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
/* Form Field Styling */
|
||||
.form-control:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
|
||||
/* Colored Header Card */
|
||||
.candidate-header-card {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
|
||||
color: white;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
.candidate-header-card h1 {
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
|
||||
/* Profile Image Upload Styling */
|
||||
.profile-image-upload {
|
||||
border: 2px dashed var(--kaauh-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.heroicon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
vertical-align: text-bottom;
|
||||
stroke: currentColor;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
.profile-image-upload:hover {
|
||||
border-color: var(--kaauh-teal);
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
|
||||
.profile-image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--kaauh-teal);
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.current-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--kaauh-teal);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
/* Breadcrumb Styling */
|
||||
.breadcrumb {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: ">";
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Alert Styling */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: auto;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Current Profile Section */
|
||||
.current-profile {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.current-profile h6 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="candidate-header-card">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap">
|
||||
<div class="flex-grow-1">
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-user-edit"></i>
|
||||
{% trans "Update Candidate:" %} {{ object.name }}
|
||||
</h1>
|
||||
<p class="text-white opacity-75 mb-0">{% trans "Edit candidate information and details" %}</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-1">
|
||||
<a href="{% url 'application_list' %}" class="btn btn-outline-light btn-sm" title="{% trans 'Back to List' %}">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span class="d-none d-sm-inline">{% trans "Back to List" %}</span>
|
||||
<div class="form-container">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'application_list' %}" class="text-decoration-none text-secondary">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applications" %}
|
||||
</a>
|
||||
{% if object.slug %}
|
||||
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-light btn-sm" title="{% trans 'View Candidate' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
<span class="d-none d-sm-inline">{% trans "View" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'application_detail' object.slug %}" class="text-decoration-none text-secondary">
|
||||
{{ object.name }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page"
|
||||
style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;">{% trans "Update" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-user-edit me-2"></i> {% trans "Update Application" %}
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
<a href="{% url 'application_delete' object.slug %}" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
<a href="{% url 'application_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Profile Info -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="current-profile">
|
||||
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
{% if object.profile_image %}
|
||||
<img src="{{ object.profile_image.url }}" alt="{{ object.name }}"
|
||||
class="current-image">
|
||||
{% else %}
|
||||
<div class="current-image d-flex align-items-center justify-content-center bg-light">
|
||||
<i class="fas fa-user text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h5 class="mb-1">{{ object.name }}</h5>
|
||||
{% if object.email %}
|
||||
<p class="text-muted mb-0">{{ object.email }}</p>
|
||||
{% endif %}
|
||||
<small class="text-muted">
|
||||
{% trans "Created" %}: {{ object.created_at|date:"d M Y" }} •
|
||||
{% trans "Last Updated" %}: {{ object.updated_at|date:"d M Y" }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h2 class="h5 mb-0 text-primary">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "Candidate Form" %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Use Crispy Forms to render fields. The two-column layout is applied to the main form content #}
|
||||
<div class="row g-4">
|
||||
{% for field in form %}
|
||||
<div class="col-md-6">
|
||||
{{ field|as_crispy_field }}
|
||||
<!-- Form Card -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
|
||||
</h5>
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="mb-0">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<hr class="mt-4 mb-4">
|
||||
<button class="btn btn-main-action" type="submit">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% trans "Update Candidate" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'candidate_update' object.slug %}" enctype="multipart/form-data" id="candidate-form">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
</form>
|
||||
<div class="d-flex gap-2">
|
||||
<button form="candidate-form" type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Update" %}
|
||||
</button>
|
||||
</div>
|
||||
</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');
|
||||
const originalImage = imagePreviewContainer ? imagePreviewContainer.innerHTML : '';
|
||||
|
||||
if (profileImageInput) {
|
||||
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) {
|
||||
if (imagePreviewContainer) {
|
||||
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 "New photo selected" %}</p>
|
||||
`;
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else if (!file && imagePreviewContainer) {
|
||||
// Reset to original if no file selected
|
||||
imagePreviewContainer.innerHTML = originalImage;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Form Validation
|
||||
const form = document.getElementById('candidate-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Basic validation
|
||||
const name = document.getElementById('id_name');
|
||||
if (name && !name.value.trim()) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Name is required." %}');
|
||||
return;
|
||||
}
|
||||
|
||||
const email = document.getElementById('id_email');
|
||||
if (email && email.value.trim() && !isValidEmail(email.value.trim())) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Warn before leaving if changes are made
|
||||
let formChanged = false;
|
||||
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
|
||||
|
||||
formInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (formChanged) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
formChanged = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -332,12 +332,12 @@
|
||||
<a href="{% url 'application_update' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
{% comment %} <button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</button> {% endcomment %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
386
templates/recruitment/candidate_delete.html
Normal file
386
templates/recruitment/candidate_delete.html
Normal file
@ -0,0 +1,386 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Candidate" %} - {{ 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;
|
||||
}
|
||||
|
||||
/* Candidate Info Card */
|
||||
.candidate-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 Profile Image */
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Header Section -->
|
||||
<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 Candidate" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "You are about to delete a candidate application. This action cannot be undone." %}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{% url 'candidate_detail' object.slug %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Candidate" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<!-- Warning Section -->
|
||||
<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">
|
||||
{% trans "Deleting this candidate will permanently remove all associated data. Please review the information below carefully before proceeding." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Candidate Information -->
|
||||
<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-user me-2"></i>
|
||||
{% trans "Candidate to be Deleted" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="candidate-info">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
{% if object.profile_image %}
|
||||
<img src="{{ object.profile_image.url }}" alt="{{ object.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.name }}</h4>
|
||||
{% if object.email %}
|
||||
<p class="text-muted mb-0">{{ object.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Position Applied" %}</div>
|
||||
<div class="info-value">
|
||||
{% if object.job_posting %}
|
||||
{{ object.job_posting.title }}
|
||||
{% else %}
|
||||
{% trans "Not specified" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.phone %}
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-phone"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Phone" %}</div>
|
||||
<div class="info-value">{{ object.phone }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-calendar"></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" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-label">{% trans "Status" %}</div>
|
||||
<div class="info-value">
|
||||
{% if object.status %}
|
||||
<span class="badge bg-info">{{ object.get_status_display }}</span>
|
||||
{% else %}
|
||||
{% trans "Not specified" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consequences -->
|
||||
<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 candidate?" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="consequence-list">
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "The candidate profile and all personal information will be permanently deleted" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "All application data and documents will be removed" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "Interview schedules and history will be deleted" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "Any associated notes and communications 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>
|
||||
|
||||
<!-- Confirmation Form -->
|
||||
<div class="card kaauh-card">
|
||||
<div class="card-body">
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<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 candidate." %}</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'candidate_detail' object.slug %}" 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 Candidate 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;
|
||||
|
||||
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);
|
||||
|
||||
// Add confirmation before final submission
|
||||
/*deleteForm.addEventListener('submit', function(e) {
|
||||
const confirmMessage = "{% trans 'Are you absolutely sure you want to delete this candidate? This action cannot be undone.' %}";
|
||||
if (!confirm(confirmMessage)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
*/
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -2,196 +2,409 @@
|
||||
{% load static i18n %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block title %}{{ title }} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* UI Variables for the KAAT-S Theme */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Form Container Styling */
|
||||
.form-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Card Styling */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Secondary Button Style */
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Form Field Styling */
|
||||
.form-control:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
|
||||
/* Breadcrumb Styling */
|
||||
.breadcrumb {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: ">";
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Alert Styling */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: auto;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Current Profile Section */
|
||||
.current-profile {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.current-profile h6 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.current-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--kaauh-teal);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="form-container">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'source_list' %}" class="text-decoration-none text-secondary">
|
||||
<i class="fas fa-plug me-1"></i> {% trans "Sources" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if source %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none text-secondary">
|
||||
{{ source.name }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page"
|
||||
style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;">{% trans "Update" %}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item active" aria-current="page"
|
||||
style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;">{% trans "Create" %}</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-plug me-2"></i> {{ title }}
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
{% if source %}
|
||||
<a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
<a href="{% url 'source_delete' source.pk %}" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Sources" %}
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if source %}
|
||||
<!-- Current Source Info -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="current-profile">
|
||||
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="current-image d-flex align-items-center justify-content-center bg-light">
|
||||
<i class="fas fa-plug text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-1">{{ source.name }}</h5>
|
||||
{% if source.source_type %}
|
||||
<p class="text-muted mb-0">{% trans "Type" %}: {{ source.get_source_type_display }}</p>
|
||||
{% endif %}
|
||||
{% if source.ip_address %}
|
||||
<p class="text-muted mb-0">{% trans "IP Address" %}: {{ source.ip_address }}</p>
|
||||
{% endif %}
|
||||
<small class="text-muted">
|
||||
{% trans "Created" %}: {{ source.created_at|date:"d M Y" }} •
|
||||
{% trans "Last Updated" %}: {{ source.updated_at|date:"d M Y" }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{% for error in form.non_field_errors %}
|
||||
<!-- Form Card -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
|
||||
</h5>
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="mb-0">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate id="source-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.name|add_class:"form-control" }}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.name.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
||||
{{ form.source_type.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.source_type|add_class:"form-select" }}
|
||||
{% if form.source_type.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.source_type.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.source_type.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
||||
{{ form.ip_address.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.ip_address|add_class:"form-control" }}
|
||||
{% if form.ip_address.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.ip_address.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.ip_address.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.trusted_ips.id_for_label }}" class="form-label">
|
||||
{{ form.trusted_ips.label }}
|
||||
</label>
|
||||
{{ form.trusted_ips|add_class:"form-control" }}
|
||||
{% if form.trusted_ips.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.trusted_ips.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.trusted_ips.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
{{ form.description|add_class:"form-control" }}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.description.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.description.help_text }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }} <span class="text-danger">*</span>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
{{ form.is_active|add_class:"form-check-input" }}
|
||||
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
|
||||
{{ form.is_active.label }}
|
||||
</label>
|
||||
{{ form.name|add_class:"form-control" }}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.name.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
||||
{{ form.source_type.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.source_type }}
|
||||
{% if form.source_type.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.source_type.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.source_type.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
||||
{{ form.ip_address.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.ip_address|add_class:"form-control" }}
|
||||
{% if form.ip_address.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.ip_address.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.ip_address.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
||||
{{ form.trusted_ips.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.trusted_ips|add_class:"form-control" }}
|
||||
{% if form.trusted_ips.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.trusted_ips.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.trusted_ips.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
{{ form.description|add_class:"form-control" }}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.description.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.description.help_text }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
{{ form.is_active|add_class:"form-check-input bg-primary-theme" }}
|
||||
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
|
||||
{{ form.is_active.label }}
|
||||
</label>
|
||||
{% if form.is_active.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.is_active.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if form.is_active.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.is_active.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.is_active.help_text }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.is_active.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- API Credentials Section -->
|
||||
{% if source %}
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">{% trans "API Credentials" %}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "API Key" %}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
hx-post="{% url 'copy_to_clipboard' %}"
|
||||
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||
title="{% trans 'Copy to clipboard' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "API Secret" %}</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||
<i class="fas fa-eye" id="secret-toggle-icon"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
hx-post="{% url 'copy_to_clipboard' %}"
|
||||
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||
title="{% trans 'Copy to clipboard' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- API Credentials Section -->
|
||||
{% if source %}
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">{% trans "API Credentials" %}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "API Key" %}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
hx-post="{% url 'copy_to_clipboard' %}"
|
||||
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||
title="{% trans 'Copy to clipboard' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-key"></i> {% trans "Generate New Keys" %}
|
||||
</a>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "API Secret" %}</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||
<i class="fas fa-eye" id="secret-toggle-icon"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
hx-post="{% url 'copy_to_clipboard' %}"
|
||||
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||
title="{% trans 'Copy to clipboard' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-key"></i> {% trans "Generate New Keys" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button form="source-form" type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -200,6 +413,67 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Form Validation
|
||||
const form = document.getElementById('source-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Basic validation
|
||||
const name = document.getElementById('id_name');
|
||||
if (name && !name.value.trim()) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Source name is required." %}');
|
||||
return;
|
||||
}
|
||||
|
||||
const ipAddress = document.getElementById('id_ip_address');
|
||||
if (ipAddress && ipAddress.value.trim() && !isValidIP(ipAddress.value.trim())) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "Please enter a valid IP address." %}');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IP validation helper
|
||||
function isValidIP(ip) {
|
||||
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipRegex.test(ip);
|
||||
}
|
||||
|
||||
// Warn before leaving if changes are made
|
||||
let formChanged = false;
|
||||
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
|
||||
|
||||
formInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (formChanged) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
formChanged = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function toggleSecretVisibility() {
|
||||
const secretInput = document.getElementById('api-secret');
|
||||
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user