queryset fixed for messaging
This commit is contained in:
parent
eca1705ff8
commit
aa62abc73b
6
.env
6
.env
@ -1,3 +1,3 @@
|
||||
DB_NAME=norahuniversity
|
||||
DB_USER=norahuniversity
|
||||
DB_PASSWORD=norahuniversity
|
||||
DB_NAME=haikal_db
|
||||
DB_USER=faheed
|
||||
DB_PASSWORD=Faheed@215
|
||||
@ -897,6 +897,15 @@ class JobPostingStatusForm(forms.ModelForm):
|
||||
widgets = {
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
}
|
||||
|
||||
def clean_status(self):
|
||||
status = self.cleaned_data.get("status")
|
||||
if status == "ACTIVE":
|
||||
if self.instance and self.instance.pk:
|
||||
print(self.instance.assigned_to)
|
||||
if not self.instance.assigned_to:
|
||||
raise ValidationError("Please assign the job posting before setting it to Active.")
|
||||
return status
|
||||
|
||||
|
||||
class LinkedPostContentForm(forms.ModelForm):
|
||||
@ -2096,23 +2105,19 @@ class CandidateEmailForm(forms.Form):
|
||||
|
||||
|
||||
|
||||
|
||||
from django.forms import HiddenInput
|
||||
class MessageForm(forms.ModelForm):
|
||||
"""Form for creating and editing messages between users"""
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ["recipient", "job", "subject", "content", "message_type"]
|
||||
fields = ["job","recipient", "subject", "content", "message_type"]
|
||||
widgets = {
|
||||
"recipient": forms.Select(
|
||||
attrs={"class": "form-select", "placeholder": "Select recipient","required": True,}
|
||||
),
|
||||
"job": forms.Select(
|
||||
attrs={"class": "form-select", "placeholder": "Select job",
|
||||
"hx-get": "/en/messages/create/",
|
||||
"hx-target": "#id_recipient",
|
||||
"hx-select": "#id_recipient",
|
||||
"hx-swap": "outerHTML",}
|
||||
attrs={"class": "form-select", "placeholder": "Select job"}
|
||||
),
|
||||
"subject": forms.TextInput(
|
||||
attrs={
|
||||
@ -2216,6 +2221,8 @@ class MessageForm(forms.ModelForm):
|
||||
self.fields["recipient"].queryset = User.objects.filter(
|
||||
user_type="staff"
|
||||
).order_by("username")
|
||||
|
||||
|
||||
|
||||
def clean(self):
|
||||
"""Validate message form data"""
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
# Generated by Django 5.2.7 on 2025-12-02 10:28
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_ckeditor_5.fields
|
||||
import django_extensions.db.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0005_merge_20251202_1308'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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.AlterField(
|
||||
model_name='interview',
|
||||
name='details_url',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
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.DeleteModel(
|
||||
name='InterviewNote',
|
||||
),
|
||||
]
|
||||
@ -519,6 +519,7 @@ def job_detail(request, slug):
|
||||
job_status = status_form.cleaned_data["status"]
|
||||
form_template = job.form_template
|
||||
if job_status == "ACTIVE":
|
||||
|
||||
form_template.is_active = True
|
||||
form_template.save(update_fields=["is_active"])
|
||||
else:
|
||||
@ -535,7 +536,9 @@ def job_detail(request, slug):
|
||||
|
||||
return redirect("job_detail", slug=slug)
|
||||
else:
|
||||
messages.error(request, "Failed to update status due to validation errors.")
|
||||
error_messages = status_form.errors.get('status', [])
|
||||
formatted_errors = "<br>".join(error_messages)
|
||||
messages.error(request, f"{formatted_errors}")
|
||||
|
||||
# --- 2. Quality Metrics (JSON Aggregation) ---
|
||||
|
||||
@ -607,10 +610,13 @@ def job_detail(request, slug):
|
||||
if avg_t_in_exam_duration
|
||||
else 0
|
||||
)
|
||||
|
||||
|
||||
category_data = (
|
||||
applications.filter(ai_analysis_data__analysis_data_en__category__isnull=False)
|
||||
.values("ai_analysis_data__analysis_data_en__category")
|
||||
applications.filter(
|
||||
ai_analysis_data__analysis_data_en__category__isnull=False
|
||||
).exclude(
|
||||
ai_analysis_data__analysis_data_en__category__exact=None
|
||||
).values("ai_analysis_data__analysis_data_en__category")
|
||||
.annotate(
|
||||
application_count=Count("id"),
|
||||
category=Cast(
|
||||
@ -619,6 +625,7 @@ def job_detail(request, slug):
|
||||
)
|
||||
.order_by("ai_analysis_data__analysis_data_en__category")
|
||||
)
|
||||
|
||||
# Prepare data for Chart.js
|
||||
categories = [item["category"] for item in category_data]
|
||||
applications_count = [item["application_count"] for item in category_data]
|
||||
@ -4688,7 +4695,7 @@ def message_detail(request, message_id):
|
||||
@login_required
|
||||
def message_create(request):
|
||||
"""Create a new message"""
|
||||
from .email_service import EmailService
|
||||
from .email_service import EmailService
|
||||
if request.method == "POST":
|
||||
form = MessageForm(request.user, request.POST)
|
||||
|
||||
@ -4699,10 +4706,7 @@ def message_create(request):
|
||||
# Send email if message_type is 'email' and recipient has email
|
||||
|
||||
if message.recipient and message.recipient.email:
|
||||
|
||||
try:
|
||||
|
||||
|
||||
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
||||
subject=message.subject,
|
||||
body_message=message.content,
|
||||
@ -4730,9 +4734,35 @@ def message_create(request):
|
||||
|
||||
messages.error(request, "Please correct the errors below.")
|
||||
else:
|
||||
|
||||
form = MessageForm(request.user)
|
||||
|
||||
form.fields["job"].widget.attrs.update({"hx-get": "/en/messages/create/",
|
||||
"hx-target": "#id_recipient",
|
||||
"hx-select": "#id_recipient",
|
||||
"hx-swap": "outerHTML",})
|
||||
if request.user.user_type == "staff":
|
||||
job_id = request.GET.get("job")
|
||||
if job_id:
|
||||
job = get_object_or_404(JobPosting, id=job_id)
|
||||
applications=job.applications.all()
|
||||
applicant_users = User.objects.filter(person_profile__in=applications.values_list('person', flat=True))
|
||||
agency_users = User.objects.filter(id__in=AgencyJobAssignment.objects.filter(job=job).values_list('agency__user', flat=True))
|
||||
form.fields["recipient"].queryset = applicant_users | agency_users
|
||||
|
||||
|
||||
# form.fields["recipient"].queryset = User.objects.filter(person_profile__)
|
||||
else:
|
||||
|
||||
form.fields['recipient'].widget = HiddenInput()
|
||||
if request.method == "GET" and "HX-Request" in request.headers and request.user.user_type in ["candidate","agency"]:
|
||||
print()
|
||||
job_id = request.GET.get("job")
|
||||
if job_id:
|
||||
job = get_object_or_404(JobPosting, id=job_id)
|
||||
form.fields["recipient"].queryset = User.objects.filter(id=job.assigned_to.id)
|
||||
form.fields["recipient"].initial = job.assigned_to
|
||||
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% load static %}
|
||||
{% load static crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %}
|
||||
|
||||
@ -11,23 +11,23 @@
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
<i class="fas fa-reply"></i> Reply to Message
|
||||
<i class="fas fa-reply"></i> {% trans "Reply to Message" %}
|
||||
{% else %}
|
||||
<i class="fas fa-envelope"></i> Compose Message
|
||||
<i class="fas fa-envelope"></i> {% trans "Compose Message" %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if form.instance.parent_message %}
|
||||
<div class="alert alert-info mb-4">
|
||||
<strong>Replying to:</strong> {{ form.instance.parent_message.subject }}
|
||||
<strong>{% trans "Replying to:" %}</strong> {{ form.instance.parent_message.subject }}
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
From {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }}
|
||||
on {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
|
||||
{% trans "From" %} {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }}
|
||||
{% trans "on" %} {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
|
||||
</small>
|
||||
<div class="mt-2">
|
||||
<strong>Original message:</strong>
|
||||
<strong>{% trans "Original message:" %}</strong>
|
||||
<div class="border-start ps-3 mt-2">
|
||||
{{ form.instance.parent_message.content|linebreaks }}
|
||||
</div>
|
||||
@ -38,99 +38,17 @@
|
||||
<form method="post" id="messageForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.job.id_for_label }}" class="form-label">
|
||||
Related Job <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.job }}
|
||||
{% if form.job.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.job.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">
|
||||
Select a job if this message is related to a specific position
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.recipient.id_for_label }}" class="form-label">
|
||||
Recipient <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.recipient }}
|
||||
|
||||
{% if form.recipient.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.recipient.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">
|
||||
Select the user who will receive this message
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.message_type.id_for_label }}" class="form-label">
|
||||
Message Type <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.message_type }}
|
||||
{% if form.message_type.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.message_type.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">
|
||||
Select the type of message you're sending
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.subject.id_for_label }}" class="form-label">
|
||||
Subject <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.subject }}
|
||||
{% if form.subject.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.subject.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.content.id_for_label }}" class="form-label">
|
||||
Message <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.content }}
|
||||
{% if form.content.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.content.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">
|
||||
Write your message here. You can use line breaks and basic formatting.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{form|crispy}}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'message_list' %}" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
<a href="{% url 'message_list' %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-times"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
{% if form.instance.pk %}
|
||||
Send Reply
|
||||
{% trans "Send Reply" %}
|
||||
{% else %}
|
||||
Send Message
|
||||
{% trans "Send Message" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
@ -184,6 +102,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Character counter for subject
|
||||
const subjectField = document.getElementById('id_subject');
|
||||
const maxLength = 200;
|
||||
const charsLabel = "{% trans 'characters' %}";
|
||||
|
||||
if (subjectField) {
|
||||
// Add character counter display
|
||||
@ -194,7 +113,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
function updateCounter() {
|
||||
const remaining = maxLength - subjectField.value.length;
|
||||
counter.textContent = `${subjectField.value.length}/${maxLength} characters`;
|
||||
counter.textContent = `${subjectField.value.length}/${maxLength} ${charsLabel}`;
|
||||
if (remaining < 20) {
|
||||
counter.className = 'text-warning';
|
||||
} else {
|
||||
@ -216,19 +135,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!recipient) {
|
||||
e.preventDefault();
|
||||
alert('Please select a recipient.');
|
||||
alert("{% trans 'Please select a recipient.' %}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!subject) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a subject.');
|
||||
alert("{% trans 'Please enter a subject.' %}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a message.');
|
||||
alert("{% trans 'Please enter a message.' %}");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user