Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend

This commit is contained in:
Faheed 2025-12-02 17:17:44 +03:00
commit 760a28db67
12 changed files with 86 additions and 146 deletions

View File

@ -812,7 +812,7 @@ class NoteForm(forms.ModelForm):
), ),
} }
labels = { labels = {
"content": _("Comment"), "content": _("Note"),
} }
# def __init__(self, *args, **kwargs): # def __init__(self, *args, **kwargs):
@ -2191,7 +2191,7 @@ class MessageForm(forms.ModelForm):
self.fields["job"].queryset = JobPosting.objects.filter( self.fields["job"].queryset = JobPosting.objects.filter(
id__in=job_ids id__in=job_ids
).order_by("-created_at") ).order_by("-created_at")
print("Agency user job queryset:", self.fields["job"].queryset) print("Agency user job queryset:", self.fields["job"].queryset)
elif self.user.user_type == "candidate": elif self.user.user_type == "candidate":
# Candidates can only see jobs they applied for # Candidates can only see jobs they applied for

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-11-27 15:36 # Generated by Django 5.2.6 on 2025-12-02 10:27
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
@ -31,6 +31,18 @@ class Migration(migrations.Migration):
('end_time', models.TimeField(verbose_name='End Time')), ('end_time', models.TimeField(verbose_name='End Time')),
], ],
), ),
migrations.CreateModel(
name='EmailContent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=255, verbose_name='Subject')),
('message', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Message Body')),
],
options={
'verbose_name': 'Email Content',
'verbose_name_plural': 'Email Contents',
},
),
migrations.CreateModel( migrations.CreateModel(
name='FormStage', name='FormStage',
fields=[ fields=[
@ -57,7 +69,6 @@ class Migration(migrations.Migration):
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')), ('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')),
('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')), ('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')),
('details_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')),
('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')), ('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')),
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), ('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
@ -65,6 +76,7 @@ class Migration(migrations.Migration):
('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')), ('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')),
('password', models.CharField(blank=True, max_length=20, null=True)), ('password', models.CharField(blank=True, max_length=20, null=True)),
('zoom_gateway_response', models.JSONField(blank=True, null=True)), ('zoom_gateway_response', models.JSONField(blank=True, null=True)),
('details_url', models.JSONField(blank=True, null=True)),
('participant_video', models.BooleanField(default=True)), ('participant_video', models.BooleanField(default=True)),
('join_before_host', models.BooleanField(default=False)), ('join_before_host', models.BooleanField(default=False)),
('host_email', models.CharField(blank=True, max_length=255, null=True)), ('host_email', models.CharField(blank=True, max_length=255, null=True)),
@ -278,24 +290,6 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Applications', 'verbose_name_plural': 'Applications',
}, },
), ),
migrations.CreateModel(
name='InterviewNote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
('interview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')),
],
options={
'verbose_name': 'Interview Note',
'verbose_name_plural': 'Interview Notes',
'ordering': ['created_at'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='JobPosting', name='JobPosting',
fields=[ fields=[
@ -363,12 +357,15 @@ class Migration(migrations.Migration):
('start_date', models.DateField(db_index=True, verbose_name='Start Date')), ('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
('end_date', models.DateField(db_index=True, verbose_name='End Date')), ('end_date', models.DateField(db_index=True, verbose_name='End Date')),
('working_days', models.JSONField(verbose_name='Working Days')), ('working_days', models.JSONField(verbose_name='Working Days')),
('topic', models.CharField(max_length=255, verbose_name='Interview Topic')),
('start_time', models.TimeField(verbose_name='Start Time')), ('start_time', models.TimeField(verbose_name='Start Time')),
('end_time', models.TimeField(verbose_name='End Time')), ('end_time', models.TimeField(verbose_name='End Time')),
('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')), ('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')),
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')), ('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
('schedule_interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type')),
('physical_address', models.CharField(blank=True, max_length=255, null=True)),
('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')), ('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')), ('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')),
@ -438,6 +435,25 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'], 'ordering': ['-created_at'],
}, },
), ),
migrations.CreateModel(
name='Note',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.application', verbose_name='Application')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')),
],
options={
'verbose_name': 'Interview Note',
'verbose_name_plural': 'Interview Notes',
'ordering': ['created_at'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Notification', name='Notification',
fields=[ fields=[
@ -478,7 +494,7 @@ class Migration(migrations.Migration):
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')), ('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')), ('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')), ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
], ],
options={ options={
'verbose_name': 'Person', 'verbose_name': 'Person',

View File

@ -1,20 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-28 10:24
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='person',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.6 on 2025-12-01 12:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='bulkinterviewtemplate',
name='schedule_interview_type',
field=models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.6 on 2025-12-01 13:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_bulkinterviewtemplate_schedule_interview_type'),
]
operations = [
migrations.AddField(
model_name='bulkinterviewtemplate',
name='physical_address',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2.6 on 2025-12-01 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_bulkinterviewtemplate_physical_address'),
]
operations = [
migrations.AddField(
model_name='bulkinterviewtemplate',
name='topic',
field=models.CharField(default='', max_length=255, verbose_name='Interview Topic'),
preserve_default=False,
),
]

View File

@ -1,14 +0,0 @@
# Generated by Django 5.2.6 on 2025-12-02 10:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_alter_person_user'),
('recruitment', '0004_bulkinterviewtemplate_topic'),
]
operations = [
]

View File

@ -27,9 +27,9 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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' OPENROUTER_API_KEY ='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'
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct'
OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'qwen/qwen-2.5-7b-instruct'
# OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'openai/gpt-oss-20b'
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free' # OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
@ -623,7 +623,8 @@ def handle_resume_parsing_and_scoring(pk: int):
}} }}
If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate. If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate.
Be Clear and Direct Avoid overly indirect politeness which can add confusion.
Be strict,objective and concise and critical in your responses, and don't give inflated scores to weak candidates.
Output only valid JSONno markdown, no extra text. Output only valid JSONno markdown, no extra text.
""" """

View File

@ -720,7 +720,7 @@ def request_cvs_download(request, slug):
if not job.applications.exists(): if not job.applications.exists():
messages.warning(request, _("No applications found for this job. ZIP file generation skipped.")) messages.warning(request, _("No applications found for this job. ZIP file generation skipped."))
return redirect('job_detail', slug=slug) return redirect('job_detail', slug=slug)
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id) async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
# Provide user feedback and redirect # Provide user feedback and redirect
@ -4898,7 +4898,7 @@ def message_delete(request, message_id):
Redirects to the message list on success (either via standard redirect Redirects to the message list on success (either via standard redirect
or HTMX's hx-redirect header). or HTMX's hx-redirect header).
""" """
# 1. Retrieve the message # 1. Retrieve the message
# Use select_related to fetch linked objects efficiently for checks/logging # Use select_related to fetch linked objects efficiently for checks/logging
message = get_object_or_404( message = get_object_or_404(
@ -4909,13 +4909,13 @@ def message_delete(request, message_id):
# Only the sender or recipient can delete the message # Only the sender or recipient can delete the message
if message.sender != request.user and message.recipient != request.user: if message.sender != request.user and message.recipient != request.user:
messages.error(request, "You don't have permission to delete this message.") messages.error(request, "You don't have permission to delete this message.")
# HTMX requests should handle redirection via client-side logic (hx-redirect) # HTMX requests should handle redirection via client-side logic (hx-redirect)
if "HX-Request" in request.headers: if "HX-Request" in request.headers:
# Returning 403 or 400 is ideal, but 200 with an empty body is often accepted # Returning 403 or 400 is ideal, but 200 with an empty body is often accepted
# by HTMX and the message is shown on the next page/refresh. # by HTMX and the message is shown on the next page/refresh.
return HttpResponse(status=403) return HttpResponse(status=403)
# Standard navigation redirect # Standard navigation redirect
return redirect("message_list") return redirect("message_list")
@ -4929,7 +4929,7 @@ def message_delete(request, message_id):
# 1. Set the HTMX response header for redirection # 1. Set the HTMX response header for redirection
response = HttpResponse(status=200) response = HttpResponse(status=200)
response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER
return response return response
# Standard navigation fallback # Standard navigation fallback
return redirect("message_list") return redirect("message_list")
@ -5099,7 +5099,8 @@ def document_upload(request, slug):
if upload_target == 'person': if upload_target == 'person':
return redirect("applicant_portal_dashboard") return redirect("applicant_portal_dashboard")
else: else:
return redirect("applicant_application_detail", slug=application.slug) return render(request, 'recruitment/application_detail.html', {'application': application})
# return redirect("application_detail", slug=application.slug)
# Handle GET request for AJAX # Handle GET request for AJAX
if request.headers.get("X-Requested-With") == "XMLHttpRequest": if request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -5111,7 +5112,6 @@ def document_upload(request, slug):
def document_delete(request, document_id): def document_delete(request, document_id):
"""Delete a document""" """Delete a document"""
document = get_object_or_404(Document, id=document_id) document = get_object_or_404(Document, id=document_id)
print(document)
# Initialize variables for redirection outside of the complex logic # Initialize variables for redirection outside of the complex logic
is_htmx = "HX-Request" in request.headers is_htmx = "HX-Request" in request.headers
@ -5174,7 +5174,9 @@ def document_delete(request, document_id):
if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest": if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest":
# For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML' # For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML'
# to remove the element, or hx-redirect to navigate. # to remove the element, or hx-redirect to navigate.
return HttpResponse(status=200) response = HttpResponse(status=200)
response["HX-Refresh"] = "true" # Instruct HTMX to refresh the current view
return response
# --- Standard Navigation Fallback --- # --- Standard Navigation Fallback ---
else: else:

View File

@ -50,7 +50,7 @@
<nav class="navbar navbar-expand-lg navbar-dark sticky-top"> <nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container-fluid max-width-1600"> <div class="container-fluid max-width-1600">
<a class="navbar-brand text-white d-none d-lg-block me-4 pe-4" href="{% url 'dashboard' %}" aria-label="Home"> <a class="navbar-brand text-white d-none d-lg-block me-4 pe-4" href="{% url 'dashboard' %}" aria-label="Home">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;"> <img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
@ -206,7 +206,7 @@
</form> </form>
{% endif %} {% endif %}
</li> </li>
<li class="d-lg-none"><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'message_list' %}"> <i class="fas fa-envelope fs-5 me-3"></i> <span>{% trans "Messages" %}</span></a></li> <li class="d-lg-none"><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'message_list' %}"> <i class="fas fa-envelope fs-5 me-3"></i> <span>{% trans "Messages" %}</span></a></li>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span></a></li> <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
@ -293,10 +293,10 @@
</a> </a>
</li> </li>
<li class="nav-item me-lg-4"> <li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'interview_list' %}"> <a class="nav-link {% if request.resolver_match.url_name == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
<span class="d-flex align-items-center gap-2"> <span class="d-flex align-items-center gap-2">
<i class="fas fa-calendar-check me-2"></i> <i class="fas fa-calendar-check me-2"></i>
{% trans "Meetings" %} {% trans "Meetings & interviews" %}
</span> </span>
</a> </a>
</li> </li>

View File

@ -1,6 +1,7 @@
{% load static %} {% load static %}
{% load file_filters %} {% load file_filters %}
{% load i18n %} {% load i18n %}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center"> <div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0 text-primary">{% trans "Documents" %}</h5> <h5 class="card-title mb-0 text-primary">{% trans "Documents" %}</h5>
@ -25,12 +26,8 @@
<form <form
method="post" method="post"
action="{% url 'application_document_upload' application.slug %}"
enctype="multipart/form-data" enctype="multipart/form-data"
hx-post="{% url 'application_document_upload' application.slug %}"
hx-target="#documents-pane"
hx-select="#documents-pane"
hx-swap="outerHTML"
hx-on::after-request="bootstrap.Modal.getInstance(document.getElementById('documentUploadModal')).hide()"
> >
{% csrf_token %} {% csrf_token %}
<div class="modal-body"> <div class="modal-body">
@ -64,7 +61,7 @@
id="documentDescription" id="documentDescription"
rows="3" rows="3"
class="form-control" class="form-control"
placeholder="{% trans "Optional description..." %}" placeholder='{% trans "Optional description..." %}'
></textarea> ></textarea>
</div> </div>
</div> </div>
@ -101,22 +98,23 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<a <a
href="{% url 'document_download' document.id %}" href="{% url 'document_download' document.id %}"
class="btn btn-sm btn-outline-primary me-2" class="btn btn-sm btn-outline-primary me-2"
title="{% trans "Download" %}" title='{% trans "Download" %}'
> >
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</a> </a>
{% if user.is_superuser or application.job.assigned_to == user %} {% if user.is_superuser or application.job.assigned_to == user %}
<button <a
hx-post="{% url 'document_delete' document.id %}"
hx-confirm='{% trans "Are you sure you want to delete" %}'
type="button" type="button"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
onclick="confirmDelete({{ document.id }}, '{{ document.file.name|filename|default:"Document" }}')" title='{% trans "Delete" %}'
title="{% trans "Delete" %}"
> >
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -131,6 +129,7 @@
</div> </div>
</div> </div>
<style> <style>
.hover-bg-light:hover { .hover-bg-light:hover {
background-color: #f8f9fa; background-color: #f8f9fa;
@ -139,7 +138,7 @@
</style> </style>
<script> <script>
function confirmDelete(documentId, fileName) { /*function confirmDelete(documentId, fileName) {
var deletePrefix = "{% trans "Are you sure you want to delete" %}"; var deletePrefix = "{% trans "Are you sure you want to delete" %}";
if (confirm(deletePrefix + ' "' + fileName + '"?')) { if (confirm(deletePrefix + ' "' + fileName + '"?')) {
htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), { htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), {
@ -147,5 +146,16 @@ function confirmDelete(documentId, fileName) {
swap: 'innerHTML' swap: 'innerHTML'
}); });
} }
}
*/
function closeUploadModal() {
var modalElement = document.getElementById('documentUploadModal');
if (modalElement) {
var modal = bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.hide();
}
}
} }
</script> </script>

View File

@ -480,7 +480,7 @@
{# TAB 5 CONTENT: PARSED SUMMARY #} {# TAB 5 CONTENT: PARSED SUMMARY #}
{% if application.parsed_summary %} {% if application.parsed_summary %}
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab"> <div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5> <h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
<div class="border-start border-primary ps-3 pt-1 pb-1"> <div class="border-start border-primary ps-3 pt-1 pb-1">
@ -663,7 +663,7 @@
<i class="fas fa-eye me-1"></i> <i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %} {% trans "View Actual Resume" %}
</a> {% endcomment %} </a> {% endcomment %}
<a href="{{ application.resume.url }}" download class="btn btn-outline-primary"> <a href="{{ application.resume.url }}" download class="btn btn-outline-primary">
<i class="fas fa-download me-1"></i> <i class="fas fa-download me-1"></i>
{% trans "Download Resume" %} {% trans "Download Resume" %}