async email send issue resolved #92

Merged
ismail merged 5 commits from frontend into main 2025-12-10 14:38:45 +03:00
11 changed files with 199 additions and 159 deletions

6
.env
View File

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

View File

@ -62,7 +62,7 @@ INSTALLED_APPS = [
"django_q",
"widget_tweaks",
"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,
supporting synchronous or asynchronous dispatch.
"""
# --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
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
if not from_interview:
# Loop through ALL final customized sends
for recipient_email, custom_message in customized_sends:
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one
processed_attachments,
sender_user_id,
job_id,
hook='recruitment.tasks.email_success_hook',
)
task_ids.append(task_id)
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
customized_sends,
processed_attachments,
sender_user_id,
job_id,
hook='recruitment.tasks.email_success_hook',
)
task_ids.append(task_id)
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(
attrs={"class": "form-control", "placeholder": "192.168.1.100"}
attrs={"class": "form-control", "placeholder": "192.168.1.100",
"required":True},
),
"trusted_ips":forms.TextInput(
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():
raise ValidationError("A source with this name already exists.")
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):
@ -900,6 +909,7 @@ class HiringAgencyForm(forms.ModelForm):
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.fields['email'].required=True
self.helper.layout = Layout(
Field("name", css_class="form-control"),
@ -949,8 +959,7 @@ class HiringAgencyForm(forms.ModelForm):
# instance = self.instance
email = email.lower().strip()
if not instance.pk: # Creating new instance
print("created ....")
if HiringAgency.objects.filter(email=email).exists():
if HiringAgency.objects.filter(email=email).exists() or User.objects.filter(email=email):
raise ValidationError("An agency with this email already exists.")
else: # Editing existing instance
if (
@ -1424,6 +1433,7 @@ class CandidateEmailForm(forms.Form):
email_addresses = []
candidates=self.cleaned_data.get('to',[])
print(f"candidates are {candidates}")
if 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.")
except Exception as e:
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
return result == 1
except Exception as e:
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.,
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
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.'}
sender=get_object_or_404(User,pk=sender_user_id)
job=get_object_or_404(JobPosting,pk=job_id)
# 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.
r=_task_send_individual_email(subject, message, recipient, attachments,sender,job)
print(f"Email send result for {recipient}: {r}")
r=_task_send_individual_email(subject, custom_message, recipient_email, attachments,sender,job)
print(f"Email send result for {recipient_email}: {r}")
if r:
successful_sends += 1
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
@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"""
if not request.user.is_authenticated:
return redirect("application_signup",slug=slug)
@ -3328,13 +3328,13 @@ def message_create(request):
if message.recipient and message.recipient.email:
if request.user.user_type != "staff":
message=message.content
message=message.content
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:
email_result = async_task('recruitment.tasks._task_send_individual_email',
subject=message.subject,
body_message=message,
body_message=body,
recipient=message.recipient.email,
attachments=None,
sender=False,
@ -3960,7 +3960,7 @@ def compose_application_email(request, job_slug):
candidate_ids=request.GET.getlist('candidate_ids')
candidates=Application.objects.filter(id__in=candidate_ids)
if request.method == 'POST':
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)
form = CandidateEmailForm(job, applications, request.POST)
if form.is_valid():
print("form is valid ...")
# Get email addresses
@ -3990,7 +3992,7 @@ def compose_application_email(request, job_slug):
subject = form.cleaned_data.get('subject')
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
print(email_addresses)
email_result = send_bulk_email( #
subject=subject,
message=message,
@ -4079,7 +4081,7 @@ def compose_application_email(request, job_slug):
else:
# GET request - show the form
form = CandidateEmailForm(job, candidates,request)
form = CandidateEmailForm(job, candidates)
return render(
request,
@ -4289,11 +4291,11 @@ def application_signup(request, slug):
# gpa = form.cleaned_data["gpa"]
password = form.cleaned_data["password"]
gpa=form.cleaned_data["gpa"]
natiional_id=form.cleaned_data["national_id"]
national_id=form.cleaned_data["national_id"]
user = User.objects.create_user(
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.save()
@ -4304,7 +4306,8 @@ def application_signup(request, slug):
phone=phone,
gender=gender,
nationality=nationality,
# gpa=gpa,
gpa=gpa,
national_id=national_id,
address=address,
user = user
)
@ -4342,18 +4345,24 @@ def interview_list(request):
# Get filter parameters
status_filter = request.GET.get('status', '')
interview_type=request.GET.get('type')
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
if interview_type:
interviews=interviews.filter(interview__location_type=interview_type)
if status_filter:
interviews = interviews.filter(status=status_filter)
if job_filter:
interviews = interviews.filter(job__title__icontains=job_filter)
interviews = interviews.filter(job__slug=job_filter)
if search_query:
interviews = interviews.filter(
Q(application__person__first_name__icontains=search_query) |
Q(application__person__last_name__icontains=search_query) |
Q(application__person__email=search_query)|
Q(job__title__icontains=search_query)
)
@ -4368,6 +4377,7 @@ def interview_list(request):
'job_filter': job_filter,
'search_query': search_query,
'interviews': interviews,
'jobs':jobs
}
return render(request, 'interviews/interview_list.html', context)
@ -4381,10 +4391,8 @@ def interview_detail(request, slug):
interview = schedule.interview
reschedule_form = ScheduledInterviewForm()
if interview:
reschedule_form.initial['topic'] = interview.topic
reschedule_form.initial['start_time'] = interview.start_time
reschedule_form.initial['duration'] = interview.duration
reschedule_form.initial['topic'] = interview.interview.topic
meeting=interview.interview
context = {
'schedule': schedule,
'interview': interview,

View File

@ -27,23 +27,45 @@
{% csrf_token %}
<!-- Recipients Field -->
<!-- Recipients Field -->
<div class="mb-3">
<div class="mb-3">
<label class="form-label fw-bold">
{% trans "To" %}
</label>
<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" %}
<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>
{% 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">
<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>
{% endif %}
</div>
{% if form.to.errors %}
<div class="text-danger small mt-1">
{% for error in form.to.errors %}
@ -53,7 +75,6 @@
{% endif %}
</div>
<!-- Subject Field -->
<div class="mb-3">
<label for="{{ form.subject.id_for_label }}" class="form-label fw-bold">

View File

@ -187,7 +187,7 @@
<div class="filter-controls">
<form method="get" class="row g-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">
<option value="">{% trans "All Jobs" %}</option>
{% for job in jobs %}
@ -195,8 +195,16 @@
{{ job.title }}
</option>
{% 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>
</div>
</div>
<div class="col-md-2">
<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">
@ -211,8 +219,8 @@
<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">
<option value="">{% trans "All Types" %}</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="Remote" {% if request.GET.type == "Remote" %}selected{% endif %}>{% trans "Remote" %}</option>
<option value="Onsite" {% if request.GET.type == "Onsite" %}selected{% endif %}>{% trans "Onsite" %}</option>
</select>
</div>
<div class="col-md-3">

View File

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

View File

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

View File

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