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

@ -5069,7 +5069,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":
@ -5081,7 +5082,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
@ -5144,7 +5144,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

@ -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>