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 = {
"content": _("Comment"),
"content": _("Note"),
}
# def __init__(self, *args, **kwargs):

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.validators
@ -31,6 +31,18 @@ class Migration(migrations.Migration):
('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(
name='FormStage',
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')),
('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')),
('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')),
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('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')),
('password', models.CharField(blank=True, max_length=20, 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)),
('join_before_host', models.BooleanField(default=False)),
('host_email', models.CharField(blank=True, max_length=255, null=True)),
@ -278,24 +290,6 @@ class Migration(migrations.Migration):
'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(
name='JobPosting',
fields=[
@ -363,12 +357,15 @@ class Migration(migrations.Migration):
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
('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')),
('end_time', models.TimeField(verbose_name='End 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')),
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (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')),
('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)')),
@ -438,6 +435,25 @@ class Migration(migrations.Migration):
'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(
name='Notification',
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')),
('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')),
('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={
'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__)
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 = '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.
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.
"""

View File

@ -5099,7 +5099,8 @@ def document_upload(request, slug):
if upload_target == 'person':
return redirect("applicant_portal_dashboard")
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
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -5111,7 +5112,6 @@ def document_upload(request, slug):
def document_delete(request, document_id):
"""Delete a document"""
document = get_object_or_404(Document, id=document_id)
print(document)
# Initialize variables for redirection outside of the complex logic
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":
# 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.
return HttpResponse(status=200)
response = HttpResponse(status=200)
response["HX-Refresh"] = "true" # Instruct HTMX to refresh the current view
return response
# --- Standard Navigation Fallback ---
else:

View File

@ -293,10 +293,10 @@
</a>
</li>
<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">
<i class="fas fa-calendar-check me-2"></i>
{% trans "Meetings" %}
{% trans "Meetings & interviews" %}
</span>
</a>
</li>

View File

@ -1,6 +1,7 @@
{% load static %}
{% load file_filters %}
{% load i18n %}
<div class="card shadow-sm">
<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>
@ -25,12 +26,8 @@
<form
method="post"
action="{% url 'application_document_upload' application.slug %}"
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 %}
<div class="modal-body">
@ -64,7 +61,7 @@
id="documentDescription"
rows="3"
class="form-control"
placeholder="{% trans "Optional description..." %}"
placeholder='{% trans "Optional description..." %}'
></textarea>
</div>
</div>
@ -101,22 +98,23 @@
<div class="d-flex align-items-center">
<a
href="{% url 'document_download' document.id %}"
class="btn btn-sm btn-outline-primary me-2"
title="{% trans "Download" %}"
>
<i class="fas fa-download"></i>
href="{% url 'document_download' document.id %}"
class="btn btn-sm btn-outline-primary me-2"
title='{% trans "Download" %}'
>
<i class="fas fa-download"></i>
</a>
{% 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"
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>
</button>
</a>
{% endif %}
</div>
</div>
@ -131,6 +129,7 @@
</div>
</div>
<style>
.hover-bg-light:hover {
background-color: #f8f9fa;
@ -139,7 +138,7 @@
</style>
<script>
function confirmDelete(documentId, fileName) {
/*function confirmDelete(documentId, fileName) {
var deletePrefix = "{% trans "Are you sure you want to delete" %}";
if (confirm(deletePrefix + ' "' + fileName + '"?')) {
htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), {
@ -147,5 +146,16 @@ function confirmDelete(documentId, fileName) {
swap: 'innerHTML'
});
}
}
*/
function closeUploadModal() {
var modalElement = document.getElementById('documentUploadModal');
if (modalElement) {
var modal = bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.hide();
}
}
}
</script>