async email send issue resolved #92
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
|
||||
@ -62,7 +62,7 @@ INSTALLED_APPS = [
|
||||
"django_q",
|
||||
"widget_tweaks",
|
||||
"easyaudit",
|
||||
# "mathfilters"
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -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.")
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
@ -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"> </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 %}
|
||||
@ -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 %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user