more updates

This commit is contained in:
ismail 2025-12-02 16:55:06 +03:00
parent eca1705ff8
commit edd2d52015
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):
@ -2186,7 +2186,7 @@ class MessageForm(forms.ModelForm):
self.fields["job"].queryset = JobPosting.objects.filter(
id__in=job_ids
).order_by("-created_at")
print("Agency user job queryset:", self.fields["job"].queryset)
elif self.user.user_type == "candidate":
# 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.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

@ -713,7 +713,7 @@ def request_cvs_download(request, slug):
if not job.applications.exists():
messages.warning(request, _("No applications found for this job. ZIP file generation skipped."))
return redirect('job_detail', slug=slug)
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
# Provide user feedback and redirect
@ -4868,7 +4868,7 @@ def message_delete(request, message_id):
Redirects to the message list on success (either via standard redirect
or HTMX's hx-redirect header).
"""
# 1. Retrieve the message
# Use select_related to fetch linked objects efficiently for checks/logging
message = get_object_or_404(
@ -4879,13 +4879,13 @@ def message_delete(request, message_id):
# Only the sender or recipient can delete the message
if message.sender != request.user and message.recipient != request.user:
messages.error(request, "You don't have permission to delete this message.")
# HTMX requests should handle redirection via client-side logic (hx-redirect)
if "HX-Request" in request.headers:
# 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.
return HttpResponse(status=403)
return HttpResponse(status=403)
# Standard navigation redirect
return redirect("message_list")
@ -4899,7 +4899,7 @@ def message_delete(request, message_id):
# 1. Set the HTMX response header for redirection
response = HttpResponse(status=200)
response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER
return response
return response
# Standard navigation fallback
return redirect("message_list")
@ -5069,7 +5069,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":
@ -5081,7 +5082,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
@ -5144,7 +5144,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

@ -50,7 +50,7 @@
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<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">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
@ -206,7 +206,7 @@
</form>
{% endif %}
</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 %}
<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>
</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>

View File

@ -480,7 +480,7 @@
{# TAB 5 CONTENT: PARSED SUMMARY #}
{% if application.parsed_summary %}
<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>
<div class="border-start border-primary ps-3 pt-1 pb-1">
@ -663,7 +663,7 @@
<i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %}
</a> {% endcomment %}
<a href="{{ application.resume.url }}" download class="btn btn-outline-primary">
<i class="fas fa-download me-1"></i>
{% trans "Download Resume" %}