Merge pull request 'async email send issue resolved' (#92) from frontend into main

Reviewed-on: #92
This commit is contained in:
ismail 2025-12-10 14:38:44 +03:00
commit af341b7d7b
11 changed files with 199 additions and 159 deletions

6
.env
View File

@ -1,3 +1,3 @@
DB_NAME=norahuniversity DB_NAME=haikal_db
DB_USER=norahuniversity DB_USER=faheed
DB_PASSWORD=norahuniversity DB_PASSWORD=Faheed@215

View File

@ -62,7 +62,7 @@ INSTALLED_APPS = [
"django_q", "django_q",
"widget_tweaks", "widget_tweaks",
"easyaudit", "easyaudit",
# "mathfilters"
] ]

View File

@ -242,7 +242,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
Send bulk email to multiple recipients with HTML support and attachments, Send bulk email to multiple recipients with HTML support and attachments,
supporting synchronous or asynchronous dispatch. supporting synchronous or asynchronous dispatch.
""" """
# --- 1. Categorization and Custom Message Preparation (CORRECTED) --- # --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
if not from_interview: if not from_interview:
@ -308,19 +308,19 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None
if not from_interview: if not from_interview:
# Loop through ALL final customized sends # Loop through ALL final customized sends
for recipient_email, custom_message in customized_sends:
task_id = async_task(
'recruitment.tasks.send_bulk_email_task', task_id = async_task(
subject, 'recruitment.tasks.send_bulk_email_task',
custom_message, # Pass the custom message subject,
[recipient_email], # Pass the specific recipient as a list of one customized_sends,
processed_attachments, processed_attachments,
sender_user_id, sender_user_id,
job_id, job_id,
hook='recruitment.tasks.email_success_hook', hook='recruitment.tasks.email_success_hook',
) )
task_ids.append(task_id) task_ids.append(task_id)
logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.") logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.")

View File

@ -77,7 +77,9 @@ class SourceForm(forms.ModelForm):
} }
), ),
"ip_address": forms.TextInput( "ip_address": forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100"} attrs={"class": "form-control", "placeholder": "192.168.1.100",
"required":True},
), ),
"trusted_ips":forms.TextInput( "trusted_ips":forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100","required": False} attrs={"class": "form-control", "placeholder": "192.168.1.100","required": False}
@ -114,6 +116,13 @@ class SourceForm(forms.ModelForm):
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists(): if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
raise ValidationError("A source with this name already exists.") raise ValidationError("A source with this name already exists.")
return name return name
def clean_ip_address(self):
ip_address=self.cleaned_data.get('ip_address')
if not ip_address:
raise ValidationError(_("Ip address should not be empty"))
return ip_address
class SourceAdvancedForm(forms.ModelForm): class SourceAdvancedForm(forms.ModelForm):
@ -900,6 +909,7 @@ class HiringAgencyForm(forms.ModelForm):
self.helper.form_class = "form-horizontal" self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3" self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9" self.helper.field_class = "col-md-9"
self.fields['email'].required=True
self.helper.layout = Layout( self.helper.layout = Layout(
Field("name", css_class="form-control"), Field("name", css_class="form-control"),
@ -949,8 +959,7 @@ class HiringAgencyForm(forms.ModelForm):
# instance = self.instance # instance = self.instance
email = email.lower().strip() email = email.lower().strip()
if not instance.pk: # Creating new instance if not instance.pk: # Creating new instance
print("created ....") if HiringAgency.objects.filter(email=email).exists() or User.objects.filter(email=email):
if HiringAgency.objects.filter(email=email).exists():
raise ValidationError("An agency with this email already exists.") raise ValidationError("An agency with this email already exists.")
else: # Editing existing instance else: # Editing existing instance
if ( if (
@ -1424,6 +1433,7 @@ class CandidateEmailForm(forms.Form):
email_addresses = [] email_addresses = []
candidates=self.cleaned_data.get('to',[]) candidates=self.cleaned_data.get('to',[])
print(f"candidates are {candidates}")
if candidates: if candidates:
for candidate in candidates: for candidate in candidates:

View File

@ -1003,31 +1003,33 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
logger.info(f"Stored sent message ID {new_message.id} in DB.") logger.info(f"Stored sent message ID {new_message.id} in DB.")
except Exception as e: except Exception as e:
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}") logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
return result == 1
except Exception as e: except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True) logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
def send_bulk_email_task(subject, message, recipient_list,attachments=None,sender_user_id=None,job_id=None, hook='recruitment.tasks.email_success_hook'): def send_bulk_email_task(subject, customized_sends,attachments=None,sender_user_id=None,job_id=None, hook='recruitment.tasks.email_success_hook'):
""" """
Django-Q background task to send pre-formatted email to a list of recipients., Django-Q background task to send pre-formatted email to a list of recipients.,
Receives arguments directly from the async_task call. Receives arguments directly from the async_task call.
""" """
logger.info(f"Starting bulk email task for {len(recipient_list)} recipients") print("jhjmfhsdjhfksjhdkfjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjh")
logger.info(f"Starting bulk email task for {len(customized_sends)} recipients")
successful_sends = 0 successful_sends = 0
total_recipients = len(recipient_list) total_recipients = len(customized_sends)
if not recipient_list: if not customized_sends:
return {'success': False, 'error': 'No recipients provided to task.'} return {'success': False, 'error': 'No recipients provided to task.'}
sender=get_object_or_404(User,pk=sender_user_id) sender=get_object_or_404(User,pk=sender_user_id)
job=get_object_or_404(JobPosting,pk=job_id) job=get_object_or_404(JobPosting,pk=job_id)
# Since the async caller sends one task per recipient, total_recipients should be 1. # Since the async caller sends one task per recipient, total_recipients should be 1.
for recipient in recipient_list: for recipient_email, custom_message in customized_sends:
# The 'message' is the custom message specific to this recipient. # The 'message' is the custom message specific to this recipient.
r=_task_send_individual_email(subject, message, recipient, attachments,sender,job) r=_task_send_individual_email(subject, custom_message, recipient_email, attachments,sender,job)
print(f"Email send result for {recipient}: {r}") print(f"Email send result for {recipient_email}: {r}")
if r: if r:
successful_sends += 1 successful_sends += 1
print(f"successful_sends: {successful_sends} out of {total_recipients}") print(f"successful_sends: {successful_sends} out of {total_recipients}")

View File

@ -1018,7 +1018,7 @@ def delete_form_template(request, template_id):
@login_required @login_required
@staff_or_candidate_required @staff_or_candidate_required
def application_submit_form(request, slug): def application_submit_form(request, template_slug):
"""Display the form as a step-by-step wizard""" """Display the form as a step-by-step wizard"""
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("application_signup",slug=slug) return redirect("application_signup",slug=slug)
@ -3328,13 +3328,13 @@ def message_create(request):
if message.recipient and message.recipient.email: if message.recipient and message.recipient.email:
if request.user.user_type != "staff": if request.user.user_type != "staff":
message=message.content message=message.content
else: else:
message=message.content.append(f"\n\n Sent by: {request.user.get_full_name()} ({request.user.email})") body=message.content+f"\n\n Sent by: {request.user.get_full_name()} ({request.user.email})"
try: try:
email_result = async_task('recruitment.tasks._task_send_individual_email', email_result = async_task('recruitment.tasks._task_send_individual_email',
subject=message.subject, subject=message.subject,
body_message=message, body_message=body,
recipient=message.recipient.email, recipient=message.recipient.email,
attachments=None, attachments=None,
sender=False, sender=False,
@ -3960,7 +3960,7 @@ def compose_application_email(request, job_slug):
candidate_ids=request.GET.getlist('candidate_ids') candidate_ids=request.GET.getlist('candidate_ids')
candidates=Application.objects.filter(id__in=candidate_ids) candidates=Application.objects.filter(id__in=candidate_ids)
if request.method == 'POST': if request.method == 'POST':
candidate_ids = request.POST.getlist('candidate_ids') candidate_ids = request.POST.getlist('candidate_ids')
@ -3968,6 +3968,8 @@ def compose_application_email(request, job_slug):
applications=Application.objects.filter(id__in=candidate_ids) applications=Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, applications, request.POST) form = CandidateEmailForm(job, applications, request.POST)
if form.is_valid(): if form.is_valid():
print("form is valid ...") print("form is valid ...")
# Get email addresses # Get email addresses
@ -3990,7 +3992,7 @@ def compose_application_email(request, job_slug):
subject = form.cleaned_data.get('subject') subject = form.cleaned_data.get('subject')
# Send emails using email service (no attachments, synchronous to avoid pickle issues) # Send emails using email service (no attachments, synchronous to avoid pickle issues)
print(email_addresses)
email_result = send_bulk_email( # email_result = send_bulk_email( #
subject=subject, subject=subject,
message=message, message=message,
@ -4079,7 +4081,7 @@ def compose_application_email(request, job_slug):
else: else:
# GET request - show the form # GET request - show the form
form = CandidateEmailForm(job, candidates,request) form = CandidateEmailForm(job, candidates)
return render( return render(
request, request,
@ -4289,11 +4291,11 @@ def application_signup(request, slug):
# gpa = form.cleaned_data["gpa"] # gpa = form.cleaned_data["gpa"]
password = form.cleaned_data["password"] password = form.cleaned_data["password"]
gpa=form.cleaned_data["gpa"] gpa=form.cleaned_data["gpa"]
natiional_id=form.cleaned_data["national_id"] national_id=form.cleaned_data["national_id"]
user = User.objects.create_user( user = User.objects.create_user(
username = email,email=email,first_name=first_name,last_name=last_name,phone=phone,user_type="candidate", username = email,email=email,first_name=first_name,last_name=last_name,phone=phone,user_type="candidate",
gpa=gpa,natiional_id=natiional_id
) )
user.set_password(password) user.set_password(password)
user.save() user.save()
@ -4304,7 +4306,8 @@ def application_signup(request, slug):
phone=phone, phone=phone,
gender=gender, gender=gender,
nationality=nationality, nationality=nationality,
# gpa=gpa, gpa=gpa,
national_id=national_id,
address=address, address=address,
user = user user = user
) )
@ -4342,18 +4345,24 @@ def interview_list(request):
# Get filter parameters # Get filter parameters
status_filter = request.GET.get('status', '') status_filter = request.GET.get('status', '')
interview_type=request.GET.get('type')
job_filter = request.GET.get('job', '') job_filter = request.GET.get('job', '')
search_query = request.GET.get('q', '') print(job_filter)
search_query = request.GET.get('search', '')
jobs=JobPosting.objects.filter(status='ACTIVE')
# Apply filters # Apply filters
if interview_type:
interviews=interviews.filter(interview__location_type=interview_type)
if status_filter: if status_filter:
interviews = interviews.filter(status=status_filter) interviews = interviews.filter(status=status_filter)
if job_filter: if job_filter:
interviews = interviews.filter(job__title__icontains=job_filter) interviews = interviews.filter(job__slug=job_filter)
if search_query: if search_query:
interviews = interviews.filter( interviews = interviews.filter(
Q(application__person__first_name__icontains=search_query) | Q(application__person__first_name__icontains=search_query) |
Q(application__person__last_name__icontains=search_query) | Q(application__person__last_name__icontains=search_query) |
Q(application__person__email=search_query)|
Q(job__title__icontains=search_query) Q(job__title__icontains=search_query)
) )
@ -4368,6 +4377,7 @@ def interview_list(request):
'job_filter': job_filter, 'job_filter': job_filter,
'search_query': search_query, 'search_query': search_query,
'interviews': interviews, 'interviews': interviews,
'jobs':jobs
} }
return render(request, 'interviews/interview_list.html', context) return render(request, 'interviews/interview_list.html', context)
@ -4381,10 +4391,8 @@ def interview_detail(request, slug):
interview = schedule.interview interview = schedule.interview
reschedule_form = ScheduledInterviewForm() reschedule_form = ScheduledInterviewForm()
if interview: reschedule_form.initial['topic'] = interview.interview.topic
reschedule_form.initial['topic'] = interview.topic meeting=interview.interview
reschedule_form.initial['start_time'] = interview.start_time
reschedule_form.initial['duration'] = interview.duration
context = { context = {
'schedule': schedule, 'schedule': schedule,
'interview': interview, 'interview': interview,

View File

@ -27,23 +27,45 @@
{% csrf_token %} {% csrf_token %}
<!-- Recipients Field --> <!-- Recipients Field -->
<!-- Recipients Field --> <!-- Recipients Field -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-bold"> <label class="form-label fw-bold">
{% trans "To" %} {% trans "To" %}
</label> </label>
<div class="border rounded p-3 bg-light" style="max-height: 200px; overflow-y: auto;"> <div class="border rounded p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
{# --- 1. DATA LAYER: Render Hidden Inputs for ALL recipients --- #}
{# This ensures the backend receives every selected user, not just the visible one #}
{% for choice in form.to %}
<input type="hidden" name="{{ form.to.name }}" value="{{ choice.data.value }}">
{% endfor %}
{# --- 2. VISUAL LAYER: Show only the first one --- #}
{# We make it disabled so the user knows they can't deselect it here #}
{% for choice in form.to|slice:":1" %} {% for choice in form.to|slice:":1" %}
<div class="form-check mb-2"> <div class="form-check mb-2">
{{ choice }} <input class="form-check-input" type="checkbox" checked disabled>
<label class="form-check-label">
{{ choice.choice_label }}
</label>
</div> </div>
{% endfor %} {% endfor %}
{% if form.to|length > 0 %}
{# --- 3. SUMMARY: Show count of hidden recipients --- #}
{% if form.to|length > 1 %}
<div class="text-muted small mt-2"> <div class="text-muted small mt-2">
<i class="fas fa-info-circle me-1"></i> <i class="fas fa-info-circle me-1"></i>
{% blocktrans count total=form.to|length %}{{ total }} recipient selected{% plural %}{{ total }} recipients selected{% endblocktrans %} {# Use simple math to show remaining count #}
{% with remaining=form.to|length|add:"-1" %}
{% blocktrans count total=remaining %}
And {{ total }} other recipient
{% plural %}
And {{ total }} other recipients
{% endblocktrans %}
{% endwith %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if form.to.errors %} {% if form.to.errors %}
<div class="text-danger small mt-1"> <div class="text-danger small mt-1">
{% for error in form.to.errors %} {% for error in form.to.errors %}
@ -53,7 +75,6 @@
{% endif %} {% endif %}
</div> </div>
<!-- Subject Field --> <!-- Subject Field -->
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.subject.id_for_label }}" class="form-label fw-bold"> <label for="{{ form.subject.id_for_label }}" class="form-label fw-bold">

View File

@ -187,7 +187,7 @@
<div class="filter-controls"> <div class="filter-controls">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="job_filter" class="form-label form-label-sm">{% trans "Job" %}</label> {% comment %} <label for="job_filter" class="form-label form-label-sm">{% trans "Job" %}</label>
<select name="job" id="job_filter" class="form-select form-select-sm"> <select name="job" id="job_filter" class="form-select form-select-sm">
<option value="">{% trans "All Jobs" %}</option> <option value="">{% trans "All Jobs" %}</option>
{% for job in jobs %} {% for job in jobs %}
@ -195,8 +195,16 @@
{{ job.title }} {{ job.title }}
</option> </option>
{% endfor %} {% endfor %}
</select> {% endcomment %}
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label>
<select name="job" id="job_filter" class="form-select form-select-sm">
<option value="">{% trans "All Jobs" %}</option>
{% for job in jobs %}
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
{% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label for="status_filter" class="form-label form-label-sm">{% trans "Status" %}</label> <label for="status_filter" class="form-label form-label-sm">{% trans "Status" %}</label>
<select name="status" id="status_filter" class="form-select form-select-sm"> <select name="status" id="status_filter" class="form-select form-select-sm">
@ -211,8 +219,8 @@
<label for="type_filter" class="form-label form-label-sm">{% trans "Type" %}</label> <label for="type_filter" class="form-label form-label-sm">{% trans "Type" %}</label>
<select name="type" id="type_filter" class="form-select form-select-sm"> <select name="type" id="type_filter" class="form-select form-select-sm">
<option value="">{% trans "All Types" %}</option> <option value="">{% trans "All Types" %}</option>
<option value="remote" {% if request.GET.type == "remote" %}selected{% endif %}>{% trans "Remote" %}</option> <option value="Remote" {% if request.GET.type == "Remote" %}selected{% endif %}>{% trans "Remote" %}</option>
<option value="onsite" {% if request.GET.type == "onsite" %}selected{% endif %}>{% trans "Onsite" %}</option> <option value="Onsite" {% if request.GET.type == "Onsite" %}selected{% endif %}>{% trans "Onsite" %}</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">

View File

@ -4,7 +4,7 @@
{% block title %}{{ message.subject }}{% endblock %} {% block title %}{{ message.subject }}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid py-4">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<!-- Message Header --> <!-- Message Header -->
@ -42,7 +42,7 @@
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row g-4">
<div class="col-md-6"> <div class="col-md-6">
<strong>From:</strong> <strong>From:</strong>
<span class="text-primary"> <span class="text-primary">
@ -54,15 +54,13 @@
</span> </span>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<strong>To:</strong> <small class="text-muted d-block mb-1">{% trans "To:" %}</small>
<span class="text-primary">{{ message.recipient.get_full_name|default:message.recipient.username }}</span> <span class="fw-semibold">{{ message.recipient.get_full_name|default:message.recipient.username }}</span>
</div> </div>
</div>
<div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<strong>Type:</strong> <small class="text-muted d-block mb-1">{% trans "Type:" %}</small>
<span class="badge bg-{{ message.message_type|lower }}"> <span class="badge bg-primary-theme">
{{ message.get_message_type_display }} {{message.get_message_type_display}}
</span> </span>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -73,13 +71,11 @@
<small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small> <small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="badge bg-warning">Unread</span> <span class="badge bg-warning text-dark">{% trans "Unread" %}</span>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<strong>Created:</strong> <small class="text-muted d-block mb-1">{% trans "Created:" %}</small>
<span>{{ message.created_at|date:"M d, Y H:i" }}</span> <span>{{ message.created_at|date:"M d, Y H:i" }}</span>
</div> </div>
{% if message.job %} {% if message.job %}
@ -90,25 +86,21 @@
{% endif %} {% endif %}
</div> </div>
{% if message.parent_message %} {% if message.parent_message %}
<div class="alert alert-info"> <div class="alert alert-info mt-3 mb-0 border-0">
<strong>In reply to:</strong> <strong>{% trans "In reply to:" %}</strong>
<a href="{% url 'message_detail' message.parent_message.id %}"> <a href="{% url 'message_detail' message.parent_message.id %}" class="text-decoration-none">
{{ message.parent_message.subject }} {{ message.parent_message.subject }}
</a> </a>
<small class="text-muted d-block"> <small class="text-muted d-block mt-2">
From {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }} {% trans "From" %} {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }}
on {{ message.parent_message.created_at|date:"M d, Y H:i" }} {% trans "on" %} {{ message.parent_message.created_at|date:"M d, Y H:i" }}
</small> </small>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<!-- Message Content --> <div class="card shadow-sm border-0 mb-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Message</h6>
</div>
<div class="card-body"> <div class="card-body">
<div class="message-content"> <div class="message-content">
{{ message.content|linebreaks }} {{ message.content|linebreaks }}
@ -116,17 +108,16 @@
</div> </div>
</div> </div>
<!-- Message Thread (if this is a reply and has replies) -->
{% if message.replies.all %} {% if message.replies.all %}
<div class="card mt-4"> <div class="card shadow-sm border-0">
<div class="card-header"> <div class="card-header bg-light border-0">
<h6 class="mb-0"> <h6 class="mb-0">
<i class="fas fa-comments"></i> Replies ({{ message.replies.count }}) <i class="fas fa-comments text-primary"></i> {% trans "Replies" %} <span class="badge bg-primary">{{ message.replies.count }}</span>
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
{% for reply in message.replies.all %} {% for reply in message.replies.all %}
<div class="border-start ps-3 mb-3"> <div class="message-reply mb-3 p-3 bg-light rounded">
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<div> <div>
<strong>{{ reply.sender.get_full_name|default:reply.sender.username }}</strong> <strong>{{ reply.sender.get_full_name|default:reply.sender.username }}</strong>
@ -138,14 +129,12 @@
{{ reply.get_message_type_display }} {{ reply.get_message_type_display }}
</span> </span>
</div> </div>
<div class="reply-content"> <div class="reply-content mb-3">
{{ reply.content|linebreaks }} {{ reply.content|linebreaks }}
</div> </div>
<div class="mt-2"> <a href="{% url 'message_reply' reply.id %}" class="btn btn-sm btn-outline-primary">
<a href="{% url 'message_reply' reply.id %}" class="btn btn-sm btn-outline-info"> <i class="fas fa-reply me-1"></i> {% trans "Reply" %}
<i class="fas fa-reply"></i> Reply to this </a>
</a>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -156,31 +145,35 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_css %} {% block customCSS %}
<style> <style>
.message-content { .message-content {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
line-height: 1.6; line-height: 1.6;
padding: 1rem; padding: 1.5rem;
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 0.375rem; border-radius: 0.5rem;
border: 1px solid #dee2e6; border: 1px solid #e9ecef;
} }
.reply-content { .reply-content {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
line-height: 1.5; line-height: 1.5;
font-size: 0.9rem; font-size: 0.95rem;
} }
.border-start { .message-reply:hover {
border-left: 3px solid #0d6efd; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
} }
.ps-3 { .bg-gradient {
padding-left: 1rem; background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
}
.btn-group .btn {
font-size: 0.875rem;
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@ -1,103 +1,102 @@
{% extends "portal_base.html" %} {% extends "portal_base.html" %}
{% load static i18n %} {% load static i18n %}
{% block title %}Messages{% endblock %} {% block title %}{% trans "Messages" %}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Messages</h4> <h4 class="mb-0 text-primary-theme">{% trans "Messages" %}</h4>
<a href="{% url 'message_create' %}" class="btn btn-main-action"> <a href="{% url 'message_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Compose Message <i class="fas fa-plus"></i> {% trans "Compose Message" %}
</a> </a>
</div> </div>
<!-- Filters --> <div class="card mb-4 border-primary-theme-subtle">
<div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="status" class="form-label">Status</label> <label for="status" class="form-label">{% trans "Status" %}</label>
<select name="status" id="status" class="form-select"> <select name="status" id="status" class="form-select">
<option value="">All Status</option> <option value="">{% trans "All Status" %}</option>
<option value="read" {% if status_filter == 'read' %}selected{% endif %}>Read</option> <option value="read" {% if status_filter == 'read' %}selected{% endif %}>{% trans "Read" %}</option>
<option value="unread" {% if status_filter == 'unread' %}selected{% endif %}>Unread</option> <option value="unread" {% if status_filter == 'unread' %}selected{% endif %}>{% trans "Unread" %}</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="type" class="form-label">Type</label> <label for="type" class="form-label">{% trans "Type" %}</label>
<select name="type" id="type" class="form-select"> <select name="type" id="type" class="form-select">
<option value="">All Types</option> <option value="">{% trans "All Types" %}</option>
<option value="GENERAL" {% if type_filter == 'GENERAL' %}selected{% endif %}>General</option> <option value="GENERAL" {% if type_filter == 'GENERAL' %}selected{% endif %}>{% trans "General" %}</option>
<option value="JOB_RELATED" {% if type_filter == 'JOB_RELATED' %}selected{% endif %}>Job Related</option> <option value="JOB_RELATED" {% if type_filter == 'JOB_RELATED' %}selected{% endif %}>{% trans "Job Related" %}</option>
<option value="INTERVIEW" {% if type_filter == 'INTERVIEW' %}selected{% endif %}>Interview</option> <option value="INTERVIEW" {% if type_filter == 'INTERVIEW' %}selected{% endif %}>{% trans "Interview" %}</option>
<option value="OFFER" {% if type_filter == 'OFFER' %}selected{% endif %}>Offer</option> <option value="OFFER" {% if type_filter == 'OFFER' %}selected{% endif %}>{% trans "Offer" %}</option>
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="q" class="form-label">Search</label> <label for="q" class="form-label">{% trans "Search" %}</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="q" id="q" class="form-control" <input type="text" name="q" id="q" class="form-control"
value="{{ search_query }}" placeholder="Search messages..."> value="{{ search_query }}" placeholder="{% trans 'Search messages...' %}">
<button class="btn btn-outline-secondary" type="submit"> <button class="btn btn-outline-primary-theme" type="submit">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">&nbsp;</label> <label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-secondary w-100">Filter</button> <button type="submit" class="btn btn-main-action w-100">
<i class="fa-solid fa-filter me-1"></i>{% trans "Filter" %}
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Statistics -->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="card bg-light"> <div class="card bg-primary-theme-subtle border-primary-theme-subtle">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Total Messages</h6> <h6 class="card-title text-primary-theme">{% trans "Total Messages" %}</h6>
<h3 class="text-primary">{{ total_messages }}</h3> <h3 class="text-primary-theme">{{ total_messages }}</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card bg-light"> <div class="card bg-warning-subtle border-warning-subtle">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Unread Messages</h6> <h6 class="card-title text-warning">{% trans "Unread Messages" %}</h6>
<h3 class="text-warning">{{ unread_messages }}</h3> <h3 class="text-warning">{{ unread_messages }}</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Messages List --> <div class="card border-primary-theme-subtle">
<div class="card">
<div class="card-body"> <div class="card-body">
{% if page_obj %} {% if page_obj %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Subject</th> <th>{% trans "Subject" %}</th>
<th>Sender</th> <th>{% trans "Sender" %}</th>
<th>Recipient</th> <th>{% trans "Recipient" %}</th>
<th>Type</th> <th>{% trans "Type" %}</th>
<th>Status</th> <th>{% trans "Status" %}</th>
<th>Created</th> <th>{% trans "Created" %}</th>
<th>Actions</th> <th>{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for message in page_obj %} {% for message in page_obj %}
<tr class="{% if not message.is_read %}table-secondary{% endif %}"> <tr class="{% if not message.is_read %}table-secondary-theme-light{% endif %}">
<td> <td>
<a href="{% url 'message_detail' message.id %}" <a href="{% url 'message_detail' message.id %}"
class="{% if not message.is_read %}fw-bold{% endif %}"> class="fw-bold text-primary-theme text-decoration-none">
{{ message.subject }} {{ message.subject|truncatechars:50 }}
</a> </a>
{% if message.parent_message %} {% if message.parent_message %}
<span class="badge bg-secondary ms-2">Reply</span> <span class="badge bg-secondary ms-2">Reply</span>
@ -125,16 +124,16 @@
</td> </td>
<td> <td>
{% if message.is_read %} {% if message.is_read %}
<span class="badge bg-primary-theme">Read</span> <span class="badge bg-primary-theme">{% trans "Read" %}</span>
{% else %} {% else %}
<span class="badge bg-warning">Unread</span> <span class="badge bg-warning">{% trans "Unread" %}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ message.created_at|date:"M d, Y H:i" }}</td> <td>{{ message.created_at|date:"M d, Y H:i" }}</td>
<td> <td>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{% url 'message_detail' message.id %}" <a href="{% url 'message_detail' message.id %}"
class="btn btn-sm btn-outline-primary" title="View"> class="btn btn-sm btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
{% if not message.is_read and message.recipient == request.user %} {% if not message.is_read and message.recipient == request.user %}
@ -145,14 +144,14 @@
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'message_reply' message.id %}" <a href="{% url 'message_reply' message.id %}"
class="btn btn-sm btn-outline-primary" title="Reply"> class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}">
<i class="fas fa-reply"></i> <i class="fas fa-reply"></i>
</a> </a>
{% comment %} <a href="{% url 'message_delete' message.id %}" {% comment %} <a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}" hx-post="{% url 'message_delete' message.id %}"
hx-confirm="Are you sure you want to delete this message?" hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
title="Delete"> title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> {% endcomment %} </a> {% endcomment %}
</div> </div>
@ -161,9 +160,9 @@
{% empty %} {% empty %}
<tr> <tr>
<td colspan="7" class="text-center text-muted"> <td colspan="7" class="text-center text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i> <i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>
<p class="mb-0">No messages found.</p> <p class="mb-0">{% trans "No messages found." %}</p>
<p class="small">Try adjusting your filters or compose a new message.</p> <p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -171,13 +170,12 @@
</table> </table>
</div> </div>
<!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<nav aria-label="Message pagination"> <nav aria-label="Message pagination">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}"> <a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
</a> </a>
</li> </li>
@ -186,18 +184,18 @@
{% for num in page_obj.paginator.page_range %} {% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %} {% if page_obj.number == num %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ num }}</span> <span class="page-link bg-primary-theme border-primary-theme">{{ num }}</span>
</li> </li>
{% else %} {% else %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a> <a class="page-link text-primary-theme" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}"> <a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</a> </a>
</li> </li>
@ -207,11 +205,11 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5">
<i class="fas fa-inbox fa-3x mb-3"></i> <i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>
<p class="mb-0">No messages found.</p> <p class="mb-0">{% trans "No messages found." %}</p>
<p class="small">Try adjusting your filters or compose a new message.</p> <p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
<a href="{% url 'message_create' %}" class="btn btn-main-action"> <a href="{% url 'message_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Compose Message <i class="fas fa-plus"></i> {% trans "Compose Message" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -222,7 +220,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block customJS %}
<script> <script>
// Auto-refresh unread count every 30 seconds // Auto-refresh unread count every 30 seconds
setInterval(() => { setInterval(() => {
@ -239,4 +237,4 @@ setInterval(() => {
.catch(error => console.error('Error fetching unread count:', error)); .catch(error => console.error('Error fetching unread count:', error));
}, 30000); }, 30000);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -285,7 +285,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label"> <label for="{{ form.email.id_for_label }}" class="form-label">
{{ form.email.label }} {{ form.email.label }}<span class="text-danger">*</span>
</label> </label>
{{ form.email|add_class:"form-control" }} {{ form.email|add_class:"form-control" }}
{% if form.email.errors %} {% if form.email.errors %}