update the email

This commit is contained in:
ismail 2025-10-30 19:53:26 +03:00
parent 3ff3c25734
commit c6fcb27613
32 changed files with 3279 additions and 152 deletions

View File

@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'haikal_db',
'USER': 'faheed',
'PASSWORD': 'Faheed@215',
'NAME': 'norahuniversity',
'USER': 'norahuniversity',
'PASSWORD': 'norahuniversity',
'HOST': '127.0.0.1',
'PORT': '5432',
}
@ -185,14 +185,26 @@ ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_FORMS = {'signup': 'recruitment.forms.StaffSignupForm'}
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = '10.10.1.110' #'smtp.gmail.com'
EMAIL_PORT = 2225 #587
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL = 'norahuniversity@example.com'
# Gmail SMTP credentials
# Remove the comment below if you want to use Gmail SMTP server
# EMAIL_HOST_USER = 'your_email@gmail.com'
# EMAIL_HOST_PASSWORD = 'your_password'
# Crispy Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

View File

@ -2,7 +2,7 @@
Email service for sending notifications related to agency messaging.
"""
from django.core.mail import send_mail
from django.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.html import strip_tags
@ -144,3 +144,211 @@ def send_assignment_notification_email(assignment, message_type='created'):
except Exception as e:
logger.error(f"Failed to send assignment notification email: {str(e)}")
return False
def send_interview_invitation_email(candidate, job, meeting_details=None, recipient_list=None):
"""
Send interview invitation email using HTML template.
Args:
candidate: Candidate instance
job: Job instance
meeting_details: Dictionary with meeting information (optional)
recipient_list: List of additional email addresses (optional)
Returns:
dict: Result with success status and error message if failed
"""
try:
# Prepare recipient list
recipients = []
if candidate.email:
recipients.append(candidate.email)
if recipient_list:
recipients.extend(recipient_list)
if not recipients:
return {'success': False, 'error': 'No recipient email addresses provided'}
# Prepare context for template
context = {
'candidate_name': candidate.full_name or candidate.name,
'candidate_email': candidate.email,
'candidate_phone': candidate.phone or '',
'job_title': job.title,
'department': getattr(job, 'department', ''),
'company_name': getattr(settings, 'COMPANY_NAME', 'Norah University'),
}
# Add meeting details if provided
if meeting_details:
context.update({
'meeting_topic': meeting_details.get('topic', f'Interview for {job.title}'),
'meeting_date_time': meeting_details.get('date_time', ''),
'meeting_duration': meeting_details.get('duration', '60 minutes'),
'join_url': meeting_details.get('join_url', ''),
})
# Render HTML template
html_message = render_to_string('emails/interview_invitation.html', context)
plain_message = strip_tags(html_message)
# Create email with both HTML and plain text versions
email = EmailMultiAlternatives(
subject=f'Interview Invitation: {job.title}',
body=plain_message,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'),
to=recipients,
)
email.attach_alternative(html_message, "text/html")
# Send email
email.send(fail_silently=False)
logger.info(f"Interview invitation email sent successfully to {', '.join(recipients)}")
return {
'success': True,
'recipients_count': len(recipients),
'message': f'Interview invitation sent successfully to {len(recipients)} recipient(s)'
}
except Exception as e:
error_msg = f"Failed to send interview invitation email: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False):
"""
Send bulk email to multiple recipients with HTML support and attachments.
Args:
subject: Email subject
message: Email message (can be HTML)
recipient_list: List of email addresses
request: Django request object (optional)
attachments: List of file attachments (optional)
async_task: Whether to run as background task (default: False)
Returns:
dict: Result with success status and error message if failed
"""
# Handle async task execution
if async_task_:
print("hereeeeeee")
from django_q.tasks import async_task
# Process attachments for background task serialization
# processed_attachments = []
# if attachments:
# for attachment in attachments:
# if hasattr(attachment, 'read'):
# # File-like object - save to temporary file
# filename = getattr(attachment, 'name', 'attachment')
# content_type = getattr(attachment, 'content_type', 'application/octet-stream')
# # Create temporary file
# with tempfile.NamedTemporaryFile(delete=False, suffix=f'_{filename}') as temp_file:
# content = attachment.read()
# temp_file.write(content)
# temp_file_path = temp_file.name
# # Store file info for background task
# processed_attachments.append({
# 'file_path': temp_file_path,
# 'filename': filename,
# 'content_type': content_type
# })
# elif isinstance(attachment, tuple) and len(attachment) == 3:
# # (filename, content, content_type) tuple - can be serialized directly
# processed_attachments.append(attachment)
# Queue the email sending as a background task
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
message,
recipient_list,
request,
)
logger.info(f"Bulk email queued as background task with ID: {task_id}")
return {
'success': True,
'async': True,
'task_id': task_id,
'message': f'Email queued for background sending to {len(recipient_list)} recipient(s)'
}
# Synchronous execution (default behavior)
try:
if not recipient_list:
return {'success': False, 'error': 'No recipients provided'}
# Clean recipient list and remove duplicates
clean_recipients = []
seen_emails = set()
for recipient in recipient_list:
email = recipient.strip().lower()
if email and email not in seen_emails:
clean_recipients.append(email)
seen_emails.add(email)
if not clean_recipients:
return {'success': False, 'error': 'No valid email addresses found'}
# Prepare email content
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
# Check if message contains HTML tags
is_html = '<' in message and '>' in message
if is_html:
# Create HTML email with plain text fallback
plain_message = strip_tags(message)
# Create email with both HTML and plain text versions
email = EmailMultiAlternatives(
subject=subject,
body=plain_message,
from_email=from_email,
to=clean_recipients,
)
email.attach_alternative(message, "text/html")
else:
# Plain text email
email = EmailMultiAlternatives(
subject=subject,
body=message,
from_email=from_email,
to=clean_recipients,
)
# Add attachments if provided
# if attachments:
# for attachment in attachments:
# if hasattr(attachment, 'read'):
# # File-like object
# filename = getattr(attachment, 'name', 'attachment')
# content = attachment.read()
# content_type = getattr(attachment, 'content_type', 'application/octet-stream')
# email.attach(filename, content, content_type)
# elif isinstance(attachment, tuple) and len(attachment) == 3:
# # (filename, content, content_type) tuple
# filename, content, content_type = attachment
# email.attach(filename, content, content_type)
# Send email
email.send(fail_silently=False)
logger.info(f"Bulk email sent successfully to {len(clean_recipients)} recipients")
return {
'success': True,
'recipients_count': len(clean_recipients),
'message': f'Email sent successfully to {len(clean_recipients)} recipient(s)'
}
except Exception as e:
error_msg = f"Failed to send bulk email: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}

View File

@ -642,7 +642,7 @@ class LinkedPostContentForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ['linkedin_post_formated_data']
class FormTemplateIsActiveForm(forms.ModelForm):
class Meta:
model = FormTemplate
@ -1188,14 +1188,175 @@ class ParticipantsSelectForm(forms.ModelForm):
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Participants"))
users=forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"))
class Meta:
model = JobPosting
fields = ['participants','users'] # No direct fields from Participants model
class CandidateEmailForm(forms.Form):
"""Form for composing emails to participants about a candidate"""
subject = forms.CharField(
max_length=200,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter email subject',
'required': True
}),
label=_('Subject'),
required=True
)
message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 8,
'placeholder': 'Enter your message here...',
'required': True
}),
label=_('Message'),
required=True
)
recipients = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check'
}),
label=_('Recipients'),
required=True
)
include_candidate_info = forms.BooleanField(
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
label=_('Include candidate information'),
initial=True,
required=False
)
include_meeting_details = forms.BooleanField(
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
label=_('Include meeting details'),
initial=True,
required=False
)
def __init__(self, job, candidate, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
self.candidate = candidate
# Get all participants and users for this job
recipient_choices = []
# Add job participants
for participant in job.participants.all():
recipient_choices.append(
(f'participant_{participant.id}', f'{participant.name} - {participant.designation} (Participant)')
)
# Add job users
for user in job.users.all():
recipient_choices.append(
(f'user_{user.id}', f'{user.get_full_name() or user.username} - {user.email} (User)')
)
self.fields['recipients'].choices = recipient_choices
self.fields['recipients'].initial = [choice[0] for choice in recipient_choices] # Select all by default
# Set initial subject
self.fields['subject'].initial = f'Interview Update: {candidate.name} - {job.title}'
# Set initial message with candidate and meeting info
initial_message = self._get_initial_message()
if initial_message:
self.fields['message'].initial = initial_message
def _get_initial_message(self):
"""Generate initial message with candidate and meeting information"""
message_parts = []
# Add candidate information
if self.candidate:
message_parts.append(f"Candidate Information:")
message_parts.append(f"Name: {self.candidate.name}")
message_parts.append(f"Email: {self.candidate.email}")
message_parts.append(f"Phone: {self.candidate.phone}")
# Add latest meeting information if available
latest_meeting = self.candidate.get_latest_meeting
if latest_meeting:
message_parts.append(f"\nMeeting Information:")
message_parts.append(f"Topic: {latest_meeting.topic}")
message_parts.append(f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}")
message_parts.append(f"Duration: {latest_meeting.duration} minutes")
if latest_meeting.join_url:
message_parts.append(f"Join URL: {latest_meeting.join_url}")
return '\n'.join(message_parts)
def clean_recipients(self):
"""Ensure at least one recipient is selected"""
recipients = self.cleaned_data.get('recipients')
if not recipients:
raise forms.ValidationError(_('Please select at least one recipient.'))
return recipients
def get_email_addresses(self):
"""Extract email addresses from selected recipients"""
email_addresses = []
recipients = self.cleaned_data.get('recipients', [])
for recipient in recipients:
if recipient.startswith('participant_'):
participant_id = recipient.split('_')[1]
try:
participant = Participants.objects.get(id=participant_id)
email_addresses.append(participant.email)
except Participants.DoesNotExist:
continue
elif recipient.startswith('user_'):
user_id = recipient.split('_')[1]
try:
user = User.objects.get(id=user_id)
email_addresses.append(user.email)
except User.DoesNotExist:
continue
return list(set(email_addresses)) # Remove duplicates
def get_formatted_message(self):
"""Get the formatted message with optional additional information"""
message = self.cleaned_data.get('message', '')
# Add candidate information if requested
if self.cleaned_data.get('include_candidate_info') and self.candidate:
candidate_info = f"\n\n--- Candidate Information ---\n"
candidate_info += f"Name: {self.candidate.name}\n"
candidate_info += f"Email: {self.candidate.email}\n"
candidate_info += f"Phone: {self.candidate.phone}\n"
message += candidate_info
# Add meeting details if requested
if self.cleaned_data.get('include_meeting_details') and self.candidate:
latest_meeting = self.candidate.get_latest_meeting
if latest_meeting:
meeting_info = f"\n\n--- Meeting Details ---\n"
meeting_info += f"Topic: {latest_meeting.topic}\n"
meeting_info += f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}\n"
meeting_info += f"Duration: {latest_meeting.duration} minutes\n"
if latest_meeting.join_url:
meeting_info += f"Join URL: {latest_meeting.join_url}\n"
message += meeting_info
return message

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-10-29 18:04
# Generated by Django 5.2.6 on 2025-10-30 10:22
import django.core.validators
import django.db.models.deletion

View File

@ -38,7 +38,7 @@ class Profile(models.Model):
class JobPosting(Base):
# Basic Job Information
JOB_TYPES = [
("FULL_TIME", "Full-time"),
("PART_TIME", "Part-time"),
@ -61,12 +61,12 @@ class JobPosting(Base):
help_text=_("Internal staff involved in the recruitment process for this job"),
)
participants=models.ManyToManyField('Participants',
participants=models.ManyToManyField('Participants',
blank=True,related_name="jobs_participating",
verbose_name=_("External Participant"),
help_text=_("External participants involved in the recruitment process for this job"),
)
# Core Fields
title = models.CharField(max_length=200)
department = models.CharField(max_length=100, blank=True)
@ -362,21 +362,21 @@ class JobPosting(Base):
@property
def offer_candidates_count(self):
return self.all_candidates.filter(stage="Offer").count() or 0
@property
def hired_candidates_count(self):
return self.all_candidates.filter(stage="Hired").count() or 0
@property
def vacancy_fill_rate(self):
total_positions = self.open_positions
no_of_positions_filled = self.candidates.filter(stage__in=['HIRED']).count()
if total_positions > 0:
vacancy_fill_rate = no_of_positions_filled / total_positions
else:
vacancy_fill_rate = 0.0
vacancy_fill_rate = 0.0
return vacancy_fill_rate
@ -678,12 +678,12 @@ class Candidate(Base):
).exists()
return future_meetings or today_future_meetings
# @property
# def time_to_hire(self):
# time_to_hire=self.hired_date-self.created_at
# return time_to_hire
class TrainingMaterial(Base):
@ -751,43 +751,19 @@ class ZoomMeeting(Base):
# Timestamps
def __str__(self):
return self.topic
return self.topic\
@property
def get_job(self):
try:
job=self.interview.job.first()
return job
except:
return None
return self.interview.job
@property
def get_candidate(self):
try:
candidate=self.interview.candidate.first()
return candidate
except:
return None
return self.interview.candidate
@property
def get_external_participants(self):
try:
interview=self.interview.first()
if interview:
return interview.job.participants.all()
return None
except:
return None
def get_participants(self):
return self.interview.job.participants.all()
@property
def get_users_participants(self):
try:
interview=self.interview.first()
if interview:
return interview.job.users.all()
return None
except:
return None
def get_users(self):
return self.interview.job.users.all()
class MeetingComment(Base):
"""
@ -1629,8 +1605,8 @@ class InterviewSchedule(Base):
models.Index(fields=['end_date']),
models.Index(fields=['created_by']),
]
class ScheduledInterview(Base):
"""Stores individual scheduled interviews"""
@ -1641,8 +1617,8 @@ class ScheduledInterview(Base):
related_name="scheduled_interviews",
db_index=True
)
job = models.ForeignKey(
"JobPosting", on_delete=models.CASCADE, related_name="scheduled_interviews", db_index=True
)
@ -1766,4 +1742,3 @@ class Participants(Base):
def __str__(self):
return f"{self.name} - {self.email}"

View File

@ -147,7 +147,7 @@ def format_job_description(pk):
2. **Format the Qualifications:** Organize and format the raw QUALIFICATIONS data into clear, readable sections using `<h2>` headings and `<ul>`/`<li>` bullet points. Encapsulate the entire formatted block within a single `<div>`.
3. **Format the Benefits:** Organize and format the raw Requirements data into clear, readable sections using `<h2>` headings and `<ul>`/`<li>` bullet points. Encapsulate the entire formatted block within a single `<div>`.
4. **Application Instructions:** Organize and format the raw Requirements data into clear, readable sections using `<h2>` headings and `<ul>`/`<li>` bullet points. Encapsulate the entire formatted block within a single `<div>`.
**TASK 2: LinkedIn Post Creation**
1. **Write the Post:** Create an engaging, professional, and concise LinkedIn post (maximum 1300 characters) summarizing the opportunity.
@ -744,3 +744,72 @@ def sync_candidate_to_source_task(candidate_id, source_id):
error_msg = f"Unexpected error during sync: {str(e)}"
logger.error(error_msg, exc_info=True)
return {"success": False, "error": error_msg}
def send_bulk_email_task(subject, message, recipient_list, request=None, attachments=None):
"""
Django-Q background task to send bulk email to multiple recipients.
Args:
subject: Email subject
message: Email message (can be HTML)
recipient_list: List of email addresses
request: Django request object (optional)
attachments: List of file attachment data (optional)
Returns:
dict: Result with success status and error message if failed
"""
from .email_service import send_bulk_email
import os
logger.info(f"Starting bulk email task for {len(recipient_list)} recipients")
try:
# Process attachments - convert file data back to file objects if needed
# processed_attachments = []
# if attachments:
# for attachment in attachments:
# if isinstance(attachment, dict) and 'file_path' in attachment:
# # This is a serialized file from background task
# file_path = attachment['file_path']
# filename = attachment.get('filename', os.path.basename(file_path))
# content_type = attachment.get('content_type', 'application/octet-stream')
# try:
# with open(file_path, 'rb') as f:
# content = f.read()
# processed_attachments.append((filename, content, content_type))
# # Clean up temporary file
# try:
# os.unlink(file_path)
# except OSError:
# pass # File might already be deleted
# except FileNotFoundError:
# logger.warning(f"Attachment file not found: {file_path}")
# continue
# else:
# # Direct attachment (file object or tuple)
# processed_attachments.append(attachment)
# Call the existing send_bulk_email function synchronously within the task
result = send_bulk_email(
subject=subject,
message=message,
recipient_list=recipient_list,
request=request,
)
if result['success']:
logger.info(f"Bulk email task completed successfully for {result.get('recipients_count', len(recipient_list))} recipients")
else:
logger.error(f"Bulk email task failed: {result.get('error', 'Unknown error')}")
return result
except Exception as e:
error_msg = f"Critical error in bulk email task: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}

View File

@ -65,7 +65,7 @@ urlpatterns = [
path('forms/builder/<slug:template_slug>/', views.form_builder, name='form_builder'),
path('forms/', views.form_templates_list, name='form_templates_list'),
path('forms/create-template/', views.create_form_template, name='create_form_template'),
path('jobs/<slug:slug>/edit_linkedin_post_content/',views.edit_linkedin_post_content,name='edit_linkedin_post_content'),
path('jobs/<slug:slug>/candidate_screening_view/', views.candidate_screening_view, name='candidate_screening_view'),
path('jobs/<slug:slug>/candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'),
@ -225,5 +225,8 @@ urlpatterns = [
path('participants/create/', views_frontend.ParticipantsCreateView.as_view(), name='participants_create'),
path('participants/<slug:slug>/', views_frontend.ParticipantsDetailView.as_view(), name='participants_detail'),
path('participants/<slug:slug>/update/', views_frontend.ParticipantsUpdateView.as_view(), name='participants_update'),
path('participants/<slug:slug>/delete/', views_frontend.ParticipantsDeleteView.as_view(), name='participants_delete'),
path('participants/<slug:slug>/delete/', views_frontend.ParticipantsDeleteView.as_view(), name='participants_delete'),
# Email composition URLs
path('jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/compose-email/', views.compose_candidate_email, name='compose_candidate_email'),
]

View File

@ -41,7 +41,8 @@ from .forms import (
AgencyAccessLinkForm,
AgencyJobAssignmentForm,
LinkedPostContentForm,
ParticipantsSelectForm
ParticipantsSelectForm,
CandidateEmailForm
)
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
from rest_framework import viewsets
@ -415,12 +416,12 @@ def job_detail(request, slug):
)
)
total_candidates=applicants.count()
avg_match_score_result = candidates_with_score.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
avg_match_score = round(avg_match_score_result or 0, 1)
avg_match_score_result = candidates_with_score.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
avg_match_score = round(avg_match_score_result or 0, 1)
high_potential_count = candidates_with_score.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count()
high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0
# --- 3. Time Metrics (Duration Aggregation) ---
# Metric: Average Time from Applied to Interview (T2I)
@ -539,14 +540,14 @@ def edit_linkedin_post_content(request,slug):
else:
messages.error(request,"Error update the Linkedin Post content")
return redirect('job_detail',job.slug)
else:
linkedin_content_form=LinkedPostContentForm()
return redirect('job_detail',job.slug)
def kaauh_career(request):
@ -1434,41 +1435,41 @@ def candidate_update_status(request, slug):
@login_required
def candidate_interview_view(request,slug):
job = get_object_or_404(JobPosting,slug=slug)
if request.method == "POST":
form = ParticipantsSelectForm(request.POST, instance=job)
print(form.errors)
if form.is_valid():
# Save the main instance (JobPosting)
job_instance = form.save(commit=False)
job_instance.save()
# MANUALLY set the M2M relationships based on submitted data
job_instance.participants.set(form.cleaned_data['participants'])
job_instance.users.set(form.cleaned_data['users'])
messages.success(request, "Interview participants updated successfully.")
return redirect("candidate_interview_view", slug=job.slug)
else:
# 🛑 FIX: Explicitly pass the initial data for M2M fields
initial_data = {
'participants': job.participants.all(),
'users': job.users.all(),
}
form = ParticipantsSelectForm(instance=job, initial=initial_data)
else:
form = ParticipantsSelectForm(instance=job)
context = {
"job":job,
"candidates":job.interview_candidates,
'current_stage':'Interview',
'form':form
'form':form,
'participants_count': job.participants.count() + job.users.count(),
}
return render(request,"recruitment/candidate_interview_view.html",context)
@ -3557,3 +3558,132 @@ def api_candidate_detail(request, candidate_id):
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
@login_required
def compose_candidate_email(request, job_slug, candidate_slug):
"""Compose email to participants about a candidate"""
from .email_service import send_bulk_email
job = get_object_or_404(JobPosting, slug=job_slug)
candidate = get_object_or_404(Candidate, slug=candidate_slug, job=job)
if request.method == 'POST':
form = CandidateEmailForm(job, candidate, request.POST)
if form.is_valid():
# Get email addresses
email_addresses = form.get_email_addresses()
if not email_addresses:
messages.error(request, 'No valid email addresses found for selected recipients.')
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
'candidate': candidate
})
# Check if this is an interview invitation
subject = form.cleaned_data.get('subject', '').lower()
is_interview_invitation = 'interview' in subject or 'meeting' in subject
if is_interview_invitation:
# Use HTML template for interview invitations
meeting_details = None
if form.cleaned_data.get('include_meeting_details'):
# Try to get meeting details from candidate
meeting_details = {
'topic': f'Interview for {job.title}',
'date_time': getattr(candidate, 'interview_date', 'To be scheduled'),
'duration': '60 minutes',
'join_url': getattr(candidate, 'meeting_url', ''),
}
from .email_service import send_interview_invitation_email
email_result = send_interview_invitation_email(
candidate=candidate,
job=job,
meeting_details=meeting_details,
recipient_list=email_addresses
)
else:
# Get formatted message for regular emails
message = form.get_formatted_message()
subject = form.cleaned_data.get('subject')
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
email_result = send_bulk_email(
subject=subject,
message=message,
recipient_list=email_addresses,
request=request,
async_task_=False # Changed to False to avoid pickle issues
)
if email_result['success']:
messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).')
# For HTMX requests, return success response
if 'HX-Request' in request.headers:
return JsonResponse({
'success': True,
'message': f'Email sent successfully to {len(email_addresses)} recipient(s).'
})
return redirect('candidate_interview_view', slug=job.slug)
else:
messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}')
# For HTMX requests, return error response
if 'HX-Request' in request.headers:
return JsonResponse({
'success': False,
'error': email_result.get("message", "Failed to send email")
})
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
'candidate': candidate
})
# except Exception as e:
# logger.error(f"Error sending candidate email: {e}")
# messages.error(request, f'An error occurred while sending the email: {str(e)}')
# # For HTMX requests, return error response
# if 'HX-Request' in request.headers:
# return JsonResponse({
# 'success': False,
# 'error': f'An error occurred while sending the email: {str(e)}'
# })
# return render(request, 'includes/email_compose_form.html', {
# 'form': form,
# 'job': job,
# 'candidate': candidate
# })
else:
# Form validation errors
print(form.errors)
messages.error(request, 'Please correct the errors below.')
# For HTMX requests, return error response
if 'HX-Request' in request.headers:
return JsonResponse({
'success': False,
'error': 'Please correct the form errors and try again.'
})
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
'candidate': candidate
})
else:
# GET request - show the form
form = CandidateEmailForm(job, candidate)
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
'candidate': candidate
})

View File

@ -133,7 +133,7 @@
data-bs-auto-close="outside"
data-bs-offset="0, 16" {# Vertical offset remains 16px to prevent clipping #}
>
{% if user.profile.profile_image %}
{% if user.profile and user.profile.profile_image %}
<img src="{{ user.profile.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
style="width: 36px; height: 36px; object-fit: cover; background-color: var(--kaauh-teal); display: inline-block; vertical-align: middle;"
title="{% trans 'Your account' %}">
@ -151,7 +151,7 @@
<li class="px-4 py-3 ">
<div class="d-flex align-items-center">
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
{% if user.profile.profile_image %}
{% if user.profile and user.profile.profile_image %}
<img src="{{ user.profile.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar shadow-sm border"
style="width: 44px; height: 44px; object-fit: cover; background-color: var(--kaauh-teal); display: block;"
title="{% trans 'Your account' %}">

View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Interview Invitation</title>
<style>
/* Basic reset and typography */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333333;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
/* Container for the main content */
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Header styling */
.header {
text-align: center;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.header h1 {
color: #007bff;
font-size: 24px;
margin: 0;
}
/* Section headings */
.section-header {
color: #007bff;
font-size: 18px;
margin-top: 25px;
margin-bottom: 10px;
border-left: 4px solid #007bff;
padding-left: 10px;
}
/* Key detail rows */
.detail-row {
margin-bottom: 10px;
}
.detail-row strong {
display: inline-block;
width: 120px;
color: #555555;
}
/* Button style for the Join URL */
.button {
display: block;
width: 80%;
margin: 25px auto;
padding: 12px 0;
background-color: #28a745; /* Success/Go color */
color: #ffffff;
text-align: center;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
}
/* Footer/closing section */
.footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px dashed #cccccc;
text-align: center;
font-size: 14px;
color: #777777;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Interview Confirmation</h1>
</div>
<p>Dear <strong>{{ candidate_name }}</strong>,</p>
<p>Thank you for your interest in the position. We are pleased to invite you to a virtual interview. Please find the details below.</p>
<h2 class="section-header">Interview Details</h2>
<div class="detail-row">
<strong>Topic:</strong> {{ meeting_topic }}
</div>
<div class="detail-row">
<strong>Date & Time:</strong> <strong>{{ meeting_date_time }}</strong>
</div>
<div class="detail-row">
<strong>Duration:</strong> {{ meeting_duration }}
</div>
{% if join_url %}
<a href="{{ join_url }}" class="button" target="_blank">
Join Interview Now
</a>
<p style="text-align: center; font-size: 14px; color: #777;">Please click the button above to join the meeting at the scheduled time.</p>
{% endif %}
<h2 class="section-header">Your Information</h2>
<div class="detail-row">
<strong>Name:</strong> {{ candidate_name }}
</div>
<div class="detail-row">
<strong>Email:</strong> {{ candidate_email }}
</div>
{% if candidate_phone %}
<div class="detail-row">
<strong>Phone:</strong> {{ candidate_phone }}
</div>
{% endif %}
{% if job_title %}
<h2 class="section-header">Position Details</h2>
<div class="detail-row">
<strong>Position:</strong> {{ job_title }}
</div>
{% if department %}
<div class="detail-row">
<strong>Department:</strong> {{ department }}
</div>
{% endif %}
{% endif %}
<div class="footer">
<p>We look forward to speaking with you.</p>
<p>If you have any questions, please reply to this email.</p>
<p>Best regards,<br>The {{ company_name|default:"Norah University" }} Team</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,509 @@
{% load i18n %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-envelope me-2"></i>
{% trans "Compose Email" %}
</h5>
</div>
<div class="card-body">
<form method="post" id="email-compose-form" action="{% url 'compose_candidate_email' job.slug candidate.slug %}">
{% csrf_token %}
<!-- Subject Field -->
<div class="mb-3">
<label for="{{ form.subject.id_for_label }}" class="form-label fw-bold">
{% trans "Subject" %}
</label>
{{ form.subject }}
{% if form.subject.errors %}
<div class="text-danger small mt-1">
{% for error in form.subject.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Recipients Field -->
<div class="mb-3">
<label class="form-label fw-bold">
{% trans "Recipients" %}
</label>
<div class="border rounded p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
{% for choice in form.recipients %}
<div class="form-check mb-2">
{{ choice }}
</div>
{% endfor %}
</div>
{% if form.recipients.errors %}
<div class="text-danger small mt-1">
{% for error in form.recipients.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Message Field -->
<div class="mb-3">
<label for="{{ form.message.id_for_label }}" class="form-label fw-bold">
{% trans "Message" %}
</label>
{{ form.message }}
{% if form.message.errors %}
<div class="text-danger small mt-1">
{% for error in form.message.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Options Checkboxes -->
<div class="mb-4">
<div class="row">
<div class="col-md-6">
<div class="form-check">
<input type="checkbox"
class="form-check-input"
name="{{ form.include_candidate_info.name }}"
id="{{ form.include_candidate_info.id_for_label }}"
{% if form.include_candidate_info.value %}checked{% endif %}>
<label class="form-check-label" for="{{ form.include_candidate_info.id_for_label }}">
{{ form.include_candidate_info.label }}
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input type="checkbox"
class="form-check-input"
name="{{ form.include_meeting_details.name }}"
id="{{ form.include_meeting_details.id_for_label }}"
{% if form.include_meeting_details.value %}checked{% endif %}>
<label class="form-check-label" for="{{ form.include_meeting_details.id_for_label }}">
{{ form.include_meeting_details.label }}
</label>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
<i class="fas fa-info-circle me-1"></i>
{% trans "Email will be sent to all selected recipients" %}
</div>
<div>
<button type="button"
class="btn btn-secondary me-2"
data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>
{% trans "Cancel" %}
</button>
<button type="submit"
class="btn btn-primary"
id="send-email-btn">
<i class="fas fa-paper-plane me-1"></i>
{% trans "Send Email" %}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="email-loading-overlay" class="d-none">
<div class="d-flex justify-content-center align-items-center" style="min-height: 200px;">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{% trans "Loading..." %}</span>
</div>
<div class="mt-2">
{% trans "Sending email..." %}
</div>
</div>
</div>
</div>
<!-- Success/Error Messages Container -->
<div id="email-messages-container"></div>
</div>
<style>
.card {
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-radius: 8px;
}
.card-header {
border-radius: 8px 8px 0 0 !important;
border-bottom: none;
}
.form-control:focus {
border-color: #00636e;
box-shadow: 0 0 0 0.2rem rgba(0,99,110,0.25);
}
.btn-primary {
background-color: #00636e;
border-color: #00636e;
}
.btn-primary:hover {
background-color: #004a53;
border-color: #004a53;
}
.form-check-input:checked {
background-color: #00636e;
border-color: #00636e;
}
.border {
border-color: #dee2e6 !important;
}
.bg-light {
background-color: #f8f9fa !important;
}
.text-danger {
color: #dc3545 !important;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('email-compose-form');
const sendBtn = document.getElementById('send-email-btn');
const loadingOverlay = document.getElementById('email-loading-overlay');
const messagesContainer = document.getElementById('email-messages-container');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
// Show loading state
if (sendBtn) {
sendBtn.disabled = true;
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> {% trans "Sending..." %}';
}
if (loadingOverlay) {
loadingOverlay.classList.remove('d-none');
}
// Clear previous messages
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
// Submit form via fetch
const formData = new FormData(form);
fetch(form.action || window.location.href, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'HX-Request': 'true'
}
})
.then(response => response.json())
.then(data => {
// Hide loading state
if (sendBtn) {
sendBtn.disabled = false;
sendBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %}';
}
if (loadingOverlay) {
loadingOverlay.classList.add('d-none');
}
// Show result message
if (data.success) {
showMessage(data.message || 'Email sent successfully!', 'success');
// Close modal after a short delay
setTimeout(() => {
const modal = form.closest('.modal');
if (modal) {
const bootstrapModal = bootstrap.Modal.getInstance(modal);
if (bootstrapModal) {
bootstrapModal.hide();
}
}
}, 1500);
} else {
showMessage(data.error || 'Failed to send email. Please try again.', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
// Hide loading state
if (sendBtn) {
sendBtn.disabled = false;
sendBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %}';
}
if (loadingOverlay) {
loadingOverlay.classList.add('d-none');
}
showMessage('An error occurred while sending the email. Please try again.', 'danger');
});
});
}
function showMessage(message, type) {
if (!messagesContainer) return;
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle';
const messageHtml = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
<i class="fas ${icon} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
messagesContainer.innerHTML = messageHtml;
// Auto-hide success messages after 5 seconds
if (type === 'success') {
setTimeout(() => {
const alert = messagesContainer.querySelector('.alert');
if (alert) {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}
}, 5000);
}
}
// Form validation
function validateForm() {
let isValid = true;
const subject = form.querySelector('#{{ form.subject.id_for_label }}');
const message = form.querySelector('#{{ form.message.id_for_label }}');
const recipients = form.querySelectorAll('input[name="{{ form.recipients.name }}"]:checked');
// Clear previous validation states
form.querySelectorAll('.is-invalid').forEach(field => {
field.classList.remove('is-invalid');
});
form.querySelectorAll('.invalid-feedback').forEach(feedback => {
feedback.remove();
});
// Validate subject
if (!subject || !subject.value.trim()) {
showFieldError(subject, 'Subject is required');
isValid = false;
}
// Validate message
if (!message || !message.value.trim()) {
showFieldError(message, 'Message is required');
isValid = false;
}
// Validate recipients
if (recipients.length === 0) {
const recipientsContainer = form.querySelector('.border.rounded.p-3.bg-light');
if (recipientsContainer) {
recipientsContainer.classList.add('border-danger');
showFieldError(recipientsContainer, 'Please select at least one recipient');
}
isValid = false;
}
return isValid;
}
function showFieldError(field, message) {
if (!field) return;
field.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = message;
if (field.classList.contains('border')) {
// For container elements (like recipients)
field.parentNode.appendChild(feedback);
} else {
// For form fields
field.parentNode.appendChild(feedback);
}
}
// Character counter for message field
function setupCharacterCounter() {
const messageField = form.querySelector('#{{ form.message.id_for_label }}');
if (!messageField) return;
const counter = document.createElement('div');
counter.className = 'text-muted small mt-1';
counter.id = 'message-counter';
messageField.parentNode.appendChild(counter);
function updateCounter() {
const length = messageField.value.length;
const maxLength = 5000; // Adjust as needed
counter.textContent = `${length} / ${maxLength} characters`;
if (length > maxLength * 0.9) {
counter.classList.add('text-warning');
counter.classList.remove('text-muted');
} else {
counter.classList.remove('text-warning');
counter.classList.add('text-muted');
}
}
messageField.addEventListener('input', updateCounter);
updateCounter(); // Initial count
}
// Auto-save functionality
let autoSaveTimer;
function setupAutoSave() {
const subject = form.querySelector('#{{ form.subject.id_for_label }}');
const message = form.querySelector('#{{ form.message.id_for_label }}');
if (!subject || !message) return;
function saveDraft() {
const draftData = {
subject: subject.value,
message: message.value,
recipients: Array.from(form.querySelectorAll('input[name="{{ form.recipients.name }}"]:checked')).map(cb => cb.value),
include_candidate_info: form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked,
include_meeting_details: form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked
};
localStorage.setItem('email_draft_' + window.location.pathname, JSON.stringify(draftData));
}
function autoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(saveDraft, 2000); // Save after 2 seconds of inactivity
}
[subject, message].forEach(field => {
field.addEventListener('input', autoSave);
});
form.addEventListener('change', autoSave);
}
function loadDraft() {
const draftData = localStorage.getItem('email_draft_' + window.location.pathname);
if (!draftData) return;
try {
const draft = JSON.parse(draftData);
const subject = form.querySelector('#{{ form.subject.id_for_label }}');
const message = form.querySelector('#{{ form.message.id_for_label }}');
if (subject && draft.subject) subject.value = draft.subject;
if (message && draft.message) message.value = draft.message;
// Restore recipients
if (draft.recipients) {
form.querySelectorAll('input[name="{{ form.recipients.name }}"]').forEach(cb => {
cb.checked = draft.recipients.includes(cb.value);
});
}
// Restore checkboxes
if (draft.include_candidate_info) {
form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked = draft.include_candidate_info;
}
if (draft.include_meeting_details) {
form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked = draft.include_meeting_details;
}
// Show draft restored notification
showMessage('Draft restored from local storage', 'success');
} catch (e) {
console.error('Error loading draft:', e);
}
}
function clearDraft() {
localStorage.removeItem('email_draft_' + window.location.pathname);
}
// Initialize form enhancements
setupCharacterCounter();
setupAutoSave();
// Load draft on page load
setTimeout(loadDraft, 100);
// Clear draft on successful submission
const originalSubmitHandler = form.onsubmit;
form.addEventListener('submit', function(e) {
const isValid = validateForm();
if (!isValid) {
e.preventDefault();
e.stopPropagation();
return false;
}
// Clear draft on successful submission
setTimeout(clearDraft, 2000);
});
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl/Cmd + Enter to submit
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'TEXTAREA' || activeElement.tagName === 'INPUT')) {
form.dispatchEvent(new Event('submit'));
}
}
// Escape to cancel/close modal
if (e.key === 'Escape') {
const modal = form.closest('.modal');
if (modal) {
const bootstrapModal = bootstrap.Modal.getInstance(modal);
if (bootstrapModal) {
bootstrapModal.hide();
}
}
}
});
console.log('Email compose form initialized');
});

View File

@ -21,7 +21,7 @@
}
body {
background-color: #f0f2f5;
background-color: #f0f2f5;
font-family: 'Inter', sans-serif;
}
@ -148,7 +148,7 @@ body {
padding: 0.5rem 0.75rem;
background-color: var(--kaauh-teal-dark);
border: none;
color: white;
color: white;
border-radius: 4px;
}
.btn-copy-simple:hover {
@ -209,7 +209,7 @@ body {
<a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Meetings" %}
</a>
{# Edit and Delete Buttons #}
<div class="d-flex gap-2">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary-teal btn-sm">
@ -226,26 +226,28 @@ body {
</div>
{# ========================================================= #}
{# --- SECTION 1: PROMINENT TOP DETAILS & JOIN INFO --- #}
{# --- MAIN TITLE AT TOP --- #}
{# ========================================================= #}
<div class="row g-4 mb-5">
{# --- LEFT HALF: MAIN TOPIC & JOB CONTEXT --- #}
<div class="col-lg-6">
<div class="main-title-container">
<h1 class="text-start" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-video me-2" style="color: var(--kaauh-teal);"></i>
{{ meeting.topic|default:"[Meeting Topic N/A]" }}
<span class="status-badge bg-{{ meeting.status|lower|default:'bg-secondary' }} ms-3">
{{ meeting.status|title|default:'N/A' }}
</span>
</h1>
</div>
<div class="main-title-container mb-4">
<h1 class="text-start" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-video me-2" style="color: var(--kaauh-teal);"></i>
{{ meeting.topic|default:"[Meeting Topic N/A]" }}
<span class="status-badge bg-{{ meeting.status|lower|default:'bg-secondary' }} ms-3">
{{ meeting.status|title|default:'N/A' }}
</span>
</h1>
</div>
{# JOB CONTEXT DETAILS (Simple Divs) #}
<div class="p-3 bg-white rounded shadow-sm">
{# ========================================================= #}
{# --- SECTION 1: INTERVIEW & CONNECTION CARDS SIDE BY SIDE --- #}
{# ========================================================= #}
<div class="row g-4 mb-5 align-items-stretch">
{# --- LEFT HALF: INTERVIEW DETAIL CARD --- #}
<div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-briefcase me-2"></i> {% trans "Interview Detail" %}</h2>
<div class="detail-row-group">
<div class="detail-row-group flex-grow-1">
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Title" %}:</div><div class="detail-value-simple">{{ meeting.get_job.title|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Name" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.name|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Email" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.email|default:"N/A" }}</a></div></div>
@ -254,11 +256,11 @@ body {
</div>
</div>
{# --- RIGHT HALF: ZOOM LINK / ACTIONS --- #}
<div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm">
{# --- RIGHT HALF: CONNECTION DETAILS CARD --- #}
<div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-info-circle me-2"></i> {% trans "Connection Details" %}</h2>
<div class="detail-row-group">
<div class="detail-row-group flex-grow-1">
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Date & Time" %}:</div><div class="detail-value-simple">{{ meeting.start_time|date:"M d, Y H:i"|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Duration" %}:</div><div class="detail-value-simple">{{ meeting.duration|default:"N/A" }} {% trans "minutes" %}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Meeting ID" %}:</div><div class="detail-value-simple">{{ meeting.meeting_id|default:"N/A" }}</div></div>
@ -266,7 +268,7 @@ body {
{% if meeting.join_url %}
<div class="join-url-container pt-3">
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: 5px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
<div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong>
@ -281,15 +283,15 @@ body {
</div>
</div>
</div>
</div>
{# ========================================================= #}
{# --- SECTION 2: PERSONNEL TABLES --- #}
{# ========================================================= #}
<div class="row g-4 mt-1 mb-5">
{# --- PARTICIPANTS TABLE --- #}
<div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm">
@ -306,7 +308,7 @@ body {
</tr>
</thead>
<tbody>
{% for participant in meeting.get_external_participants %}
{% for participant in meeting.get_participants %}
<tr>
<td>{{participant.name}}</td>
<td>{{participant.designation}}</td>
@ -315,29 +317,29 @@ body {
<td>{% trans "External Participants" %}</td>
</tr>
{% endfor %}
{% for participant in meeting.get_users_participants %}
{% for user in meeting.get_users %}
<tr>
<td>{{participant.name}}</td>
<td>{{participant.designation}}</td>
<td>{{participant.email}}</td>
<td>{{participant.phone}}</td>
<td>{{user.get_full_name}}</td>
<td>Admin</td>
<td>{{user.email}}</td>
<td>{{user.phone}}</td>
<td>{% trans "System User" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# ========================================================= #}
{# --- SECTION 3: COMMENTS (CORRECTED) --- #}
{# ========================================================= #}
<div class="row g-4 mt-1">
<div class="col-lg-12">
<div class="card flex-grow-1" id="comments-card" style="height: 100%;">
<div class="card" id="comments-card" style="height: 100%;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
@ -345,29 +347,29 @@ body {
</h5>
</div>
<div class="card-body overflow-auto">
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
<div id="comment-section" class="mb-4">
{% if meeting.comments.all %}
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
<div class="comment-item mb-3 p-3">
{# Read-Only Comment View #}
<div id="comment-view-{{ comment.pk }}">
<p class="mb-1 d-flex justify-content-between align-items-start" style="font-size: 0.9rem;">
<div>
<strong>{{ comment.author.get_full_name|default:comment.author.username }}</strong>
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="comment-metadata" style="font-size: 0.9rem;">
<strong>{{ comment.author.get_full_name|default:comment.author.username }}</strong>
<span class="text-muted small ms-2">{{ comment.created_at|date:"M d, Y H:i" }}</span>
</div>
{% if comment.author == user or user.is_staff %}
<div class="btn-group btn-group-sm">
<div class="comment-actions d-flex align-items-center gap-1">
{# Edit Button: Toggles the hidden form #}
<button type="button" class="btn btn-edit-comment py-0 px-1 me-2" onclick="toggleCommentEdit('{{ comment.pk }}')" id="edit-btn-{{ comment.pk }}" title="{% trans 'Edit Comment' %}">
<button type="button" class="btn btn-edit-comment py-0 px-1" onclick="toggleCommentEdit('{{ comment.pk }}')" id="edit-btn-{{ comment.pk }}" title="{% trans 'Edit Comment' %}">
<i class="fas fa-edit"></i>
</button>
{# Delete Form: Submits a POST request #}
<form method="post" action="{% url 'delete_meeting_comment' meeting.slug comment.pk %}" style="display: inline;" id="delete-form-{{ comment.pk }}">
{% csrf_token %}
@ -377,10 +379,10 @@ body {
</form>
</div>
{% endif %}
</p>
</div>
<p class="mb-0 comment-content" style="font-size: 0.85rem; white-space: pre-wrap;">{{ comment.content|linebreaksbr }}</p>
</div>
{# Hidden Edit Form #}
<div id="comment-edit-form-{{ comment.pk }}" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--kaauh-border);">
<form method="POST" action="{% url 'edit_meeting_comment' meeting.slug comment.pk %}" id="form-{{ comment.pk }}">
@ -405,7 +407,7 @@ body {
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
</div>
<hr>
{# 2. NEW COMMENT SUBMISSION (Remains the same) #}
@ -479,7 +481,7 @@ body {
// Note: This positioning logic relies on the .join-url-container being position:relative or position:absolute
messageElement.style.left = (rect.width / 2) - (messageElement.offsetWidth / 2) + 'px';
messageElement.style.top = '-35px';
window.copyMessageTimeout = setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);
@ -520,4 +522,4 @@ body {
callback(success);
}
</script>
{% endblock %}
{% endblock %}

View File

@ -231,13 +231,14 @@
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button>
</form>
<div class="vr" style="height: 28px;"></div>
<div class="vr" style="height: 28px;"></div>
<!--manage participants for interview-->
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
data-bs-target="#jobAssignmentModal">
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %}
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{participants_count}})
</button>
</div>
</div>
{% endif %}
<div class="table-responsive">
@ -369,6 +370,14 @@
title="View Profile">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug candidate.slug %}"
hx-target="#emailModalBody"
title="Email Participants">
<i class="fas fa-envelope"></i>
</button>
{% if candidate.get_latest_meeting %}
<button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal"
@ -432,7 +441,7 @@
</div>
</div>
</div>
<div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true">
@ -440,39 +449,59 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
{% csrf_token %}
<div class="modal-body">
{{ job.internal_job_id }} {{ job.title}}
<hr>
<h3>👥 {% trans "Participants" %}</h3>
{{ form.participants.errors }}
{{ form.participants }}
{{ form.participants }}
<hr>
<h3>🧑‍💼 {% trans "Users" %}</h3>
{{ form.users.errors }}
{{ form.users }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-main-action">{% trans "Save" %}</button>
<button type="submit" class="btn btn-main-action">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="emailModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
@ -579,7 +608,7 @@
$(document).ready(function() {
// Check the flag passed from the Django view
var shouldOpenModal = {{ show_modal_on_load|yesno:"true,false" }};
// If the view detected an invalid form submission (POST request), open the modal
if (shouldOpenModal) {
// Use the native Bootstrap 5 JS function to show the modal

105
test_async_email.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python
"""
Test script for async email functionality
"""
import os
import sys
import django
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
django.setup()
from django.contrib.auth.models import User
from recruitment.email_service import send_bulk_email
from recruitment.models import JobPosting, Candidate
def test_async_email():
"""Test async email sending functionality"""
print("🧪 Testing Async Email Functionality")
print("=" * 50)
try:
# Get a test user
test_user = User.objects.first()
if not test_user:
print("❌ No users found in database. Please create a user first.")
return
# Get a test job and candidate
test_job = JobPosting.objects.first()
test_candidate = Candidate.objects.first()
if not test_job or not test_candidate:
print("❌ No test job or candidate found. Please create some test data first.")
return
print(f"📧 Test User: {test_user.email}")
print(f"💼 Test Job: {test_job.title}")
print(f"👤 Test Candidate: {test_candidate.name}")
# Test synchronous email sending
print("\n1. Testing Synchronous Email Sending...")
try:
sync_result = send_bulk_email(
subject="Test Synchronous Email",
message="This is a test synchronous email from the ATS system.",
recipient_list=[test_user.email],
async_task=False
)
print(f" ✅ Sync result: {sync_result}")
except Exception as e:
print(f" ❌ Sync error: {e}")
# Test asynchronous email sending
print("\n2. Testing Asynchronous Email Sending...")
try:
async_result = send_bulk_email(
subject="Test Asynchronous Email",
message="This is a test asynchronous email from the ATS system.",
recipient_list=[test_user.email],
async_task=True
)
print(f" ✅ Async result: {async_result}")
except Exception as e:
print(f" ❌ Async error: {e}")
print("\n3. Testing Email Service Status...")
# Check Django Q configuration
try:
import django_q
from django_q.models import Task
pending_tasks = Task.objects.count()
print(f" 📊 Django Q Status: Installed, {pending_tasks} tasks in queue")
except ImportError:
print(" ⚠️ Django Q not installed")
except Exception as e:
print(f" 📊 Django Q Status: Installed but error checking status: {e}")
# Check email backend configuration
from django.conf import settings
email_backend = getattr(settings, 'EMAIL_BACKEND', 'not configured')
print(f" 📧 Email Backend: {email_backend}")
email_host = getattr(settings, 'EMAIL_HOST', 'not configured')
print(f" 🌐 Email Host: {email_host}")
email_port = getattr(settings, 'EMAIL_PORT', 'not configured')
print(f" 🔌 Email Port: {email_port}")
print("\n✅ Async email functionality test completed!")
print("💡 If emails are not being received, check:")
print(" - Email server configuration in settings.py")
print(" - Django Q cluster status (python manage.py qmonitor)")
print(" - Email logs and spam folders")
except Exception as e:
print(f"❌ Test failed with error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
test_async_email()

100
test_email_attachments.py Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,267 @@
#!/usr/bin/env python
"""
Clean test script for email attachment functionality
"""
import os
import sys
import django
from django.conf import settings
# Configure Django settings BEFORE importing any Django modules
if not settings.configured:
settings.configure(
DEBUG=True,
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
},
USE_TZ=True,
SECRET_KEY='test-secret-key',
INSTALLED_APPS=[
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'recruitment',
],
EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend',
MEDIA_ROOT='/tmp/test_media',
FILE_UPLOAD_TEMP_DIR='/tmp/test_uploads',
)
# Setup Django
django.setup()
# Now import Django modules
from django.test import TestCase, Client
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.files.base import ContentFile
import io
from unittest.mock import Mock
from recruitment.email_service import send_bulk_email
from recruitment.forms import CandidateEmailForm
from recruitment.models import JobPosting, Candidate
from django.test import RequestFactory
from django.contrib.auth.models import User
# Setup test database
from django.db import connection
from django.core.management import execute_from_command_line
def setup_test_data():
"""Create test data for email attachment testing"""
# Create test user
user = User.objects.create_user(
username='testuser',
email='test@example.com',
first_name='Test',
last_name='User'
)
# Create test job
job = JobPosting.objects.create(
title='Test Job Position',
description='This is a test job for email attachment testing.',
status='ACTIVE',
internal_job_id='TEST-001'
)
# Create test candidate
candidate = Candidate.objects.create(
first_name='John',
last_name='Doe',
email='john.doe@example.com',
phone='+1234567890',
address='123 Test Street',
job=job,
stage='Interview'
)
return user, job, candidate
def test_email_service_with_attachments():
"""Test the email service directly with attachments"""
print("Testing email service with attachments...")
# Create test files
test_files = []
# Test 1: Simple text file
text_content = "This is a test attachment content."
text_file = ContentFile(
text_content.encode('utf-8'),
name='test_document.txt'
)
test_files.append(('test_document.txt', text_file, 'text/plain'))
# Test 2: PDF content (simulated)
pdf_content = b'%PDF-1.4\n1 0 obj\n<<\n/Length 100\n>>stream\nxref\nstartxref\n1234\n5678\n/ModDate(D:20250101)\n'
pdf_file = ContentFile(
pdf_content,
name='test_document.pdf'
)
test_files.append(('test_document.pdf', pdf_file, 'application/pdf'))
# Test 3: Image file (simulated PNG header)
image_content = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01'
image_file = ContentFile(
image_content,
name='test_image.png'
)
test_files.append(('test_image.png', image_file, 'image/png'))
try:
# Test email service with attachments
result = send_bulk_email(
subject='Test Email with Attachments',
body='This is a test email with attachments.',
from_email='test@example.com',
recipient_list=['recipient@example.com'],
attachments=test_files
)
print(f"Email service result: {result}")
print("✓ Email service with attachments test passed")
return True
except Exception as e:
print(f"✗ Email service test failed: {e}")
return False
def test_candidate_email_form_with_attachments():
"""Test the CandidateEmailForm with attachments"""
print("\nTesting CandidateEmailForm with attachments...")
user, job, candidate = setup_test_data()
# Create test files for form
text_file = SimpleUploadedFile(
"test.txt",
b"This is test content for form attachment"
)
pdf_file = SimpleUploadedFile(
"test.pdf",
b"%PDF-1.4 test content"
)
form_data = {
'subject': 'Test Subject',
'body': 'Test body content',
'from_email': 'test@example.com',
'recipient_list': 'recipient@example.com',
}
files_data = {
'attachments': [text_file, pdf_file]
}
try:
form = CandidateEmailForm(data=form_data, files=files_data)
if form.is_valid():
print("✓ Form validation passed")
print(f"Form cleaned data: {form.cleaned_data}")
# Test form processing
try:
result = form.save()
print(f"✓ Form save result: {result}")
return True
except Exception as e:
print(f"✗ Form save failed: {e}")
return False
else:
print(f"✗ Form validation failed: {form.errors}")
return False
except Exception as e:
print(f"✗ Form test failed: {e}")
return False
def test_email_view_with_attachments():
"""Test the email view with attachments"""
print("\nTesting email view with attachments...")
user, job, candidate = setup_test_data()
factory = RequestFactory()
# Create a mock request with files
text_file = SimpleUploadedFile(
"test.txt",
b"This is test content for view attachment"
)
request = factory.post(
'/recruitment/send-candidate-email/',
data={
'subject': 'Test Subject',
'body': 'Test body content',
'from_email': 'test@example.com',
'recipient_list': 'recipient@example.com',
},
format='multipart'
)
request.FILES['attachments'] = [text_file]
request.user = user
try:
# Import and test the view
from recruitment.views import send_candidate_email
response = send_candidate_email(request)
print(f"View response status: {response.status_code}")
if response.status_code == 200:
print("✓ Email view test passed")
return True
else:
print(f"✗ Email view test failed with status: {response.status_code}")
return False
except Exception as e:
print(f"✗ Email view test failed: {e}")
return False
def main():
"""Run all email attachment tests"""
print("=" * 60)
print("EMAIL ATTACHMENT FUNCTIONALITY TESTS")
print("=" * 60)
# Initialize Django
django.setup()
# Create tables
from django.core.management import execute_from_command_line
execute_from_command_line(['manage.py', 'migrate', '--run-syncdb'])
results = []
# Run tests
results.append(test_email_service_with_attachments())
results.append(test_candidate_email_form_with_attachments())
results.append(test_email_view_with_attachments())
# Summary
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
passed = sum(results)
total = len(results)
print(f"Tests passed: {passed}/{total}")
if passed == total:
print("🎉 All email attachment tests passed!")
return True
else:
print("❌ Some email attachment tests failed!")
return False
if __name__ == '__main__':
success = main()
exit(0 if success else 1)

218
test_email_composition.py Normal file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Test script to verify email composition functionality
"""
import os
import sys
import django
# Setup Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
django.setup()
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from django.utils import timezone
from recruitment.models import JobPosting, Candidate
from unittest.mock import patch, MagicMock
def test_email_composition_view():
"""Test the email composition view"""
print("Testing email composition view...")
# Create test user (delete if exists)
User.objects.filter(username='testuser').delete()
user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
# Create test job
job = JobPosting.objects.create(
title='Test Job',
internal_job_id='TEST001',
description='Test job description',
status='active',
application_deadline=timezone.now() + timezone.timedelta(days=30)
)
# Add user to job participants so they appear in recipient choices
job.users.add(user)
# Create test candidate
candidate = Candidate.objects.create(
first_name='Test Candidate',
last_name='',
email='candidate@example.com',
phone='1234567890',
job=job
)
# Create client and login
client = Client()
client.login(username='testuser', password='testpass123')
# Test GET request to email composition view
url = reverse('compose_candidate_email', kwargs={
'job_slug': job.slug,
'candidate_slug': candidate.slug
})
try:
response = client.get(url)
print(f"✓ GET request successful: {response.status_code}")
if response.status_code == 200:
print("✓ Email composition form rendered successfully")
# Check if form contains expected fields
content = response.content.decode('utf-8')
if 'subject' in content.lower():
print("✓ Subject field found in form")
if 'message' in content.lower():
print("✓ Message field found in form")
if 'recipients' in content.lower():
print("✓ Recipients field found in form")
else:
print(f"✗ Unexpected status code: {response.status_code}")
except Exception as e:
print(f"✗ Error testing GET request: {e}")
# Test POST request with mock email sending
post_data = {
'subject': 'Test Subject',
'message': 'Test message content',
'recipients': ['candidate@example.com'],
'include_candidate_info': True,
'include_meeting_details': False
}
with patch('django.core.mail.send_mass_mail') as mock_send_mail:
mock_send_mail.return_value = 1
try:
response = client.post(url, data=post_data)
print(f"✓ POST request successful: {response.status_code}")
if response.status_code == 200:
# Check if JSON response is correct
try:
json_data = response.json()
if json_data.get('success'):
print("✓ Email sent successfully")
print(f"✓ Success message: {json_data.get('message')}")
else:
print(f"✗ Email send failed: {json_data.get('error')}")
except:
print("✗ Invalid JSON response")
else:
print(f"✗ Unexpected status code: {response.status_code}")
except Exception as e:
print(f"✗ Error testing POST request: {e}")
# Clean up
user.delete()
job.delete()
candidate.delete()
print("Email composition test completed!")
def test_email_form():
"""Test the CandidateEmailForm"""
print("\nTesting CandidateEmailForm...")
from recruitment.forms import CandidateEmailForm
# Create test user for form (delete if exists)
User.objects.filter(username='formuser').delete()
form_user = User.objects.create_user(
username='formuser',
email='form@example.com',
password='formpass123'
)
# Create test job and candidate for form
job = JobPosting.objects.create(
title='Test Job Form',
internal_job_id='TEST002',
description='Test job description for form',
status='active',
application_deadline=timezone.now() + timezone.timedelta(days=30)
)
# Add user to job participants so they appear in recipient choices
job.users.add(form_user)
candidate = Candidate.objects.create(
first_name='Test Candidate',
last_name='Form',
email='candidate_form@example.com',
phone='1234567890',
job=job
)
try:
# Test valid form data - get available choices from form
form = CandidateEmailForm(job, candidate)
available_choices = [choice[0] for choice in form.fields['recipients'].choices]
# Use first available choice for testing
test_recipient = available_choices[0] if available_choices else None
if test_recipient:
form = CandidateEmailForm(job, candidate, data={
'subject': 'Test Subject',
'message': 'Test message content',
'recipients': [test_recipient],
'include_candidate_info': True,
'include_meeting_details': False
})
if form.is_valid():
print("✓ Form validation passed")
print(f"✓ Cleaned recipients: {form.cleaned_data['recipients']}")
else:
print(f"✗ Form validation failed: {form.errors}")
else:
print("✗ No recipient choices available for testing")
except Exception as e:
print(f"✗ Error testing form: {e}")
try:
# Test invalid form data (empty subject)
form = CandidateEmailForm(job, candidate, data={
'subject': '',
'message': 'Test message content',
'recipients': [],
'include_candidate_info': True,
'include_meeting_details': False
})
if not form.is_valid():
print("✓ Form correctly rejected empty subject")
if 'subject' in form.errors:
print("✓ Subject field has validation error")
else:
print("✗ Form should have failed validation")
except Exception as e:
print(f"✗ Error testing invalid form: {e}")
# Clean up
job.delete()
candidate.delete()
if __name__ == '__main__':
print("Running Email Composition Tests")
print("=" * 50)
test_email_form()
test_email_composition_view()
print("\n" + "=" * 50)
print("All tests completed!")

507
test_email_form_js.html Normal file
View File

@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Compose Form Test</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1>Email Compose Form JavaScript Test</h1>
<!-- Mock form for testing JavaScript -->
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-envelope me-2"></i>
Compose Email
</h5>
</div>
<div class="card-body">
<form method="post" id="email-compose-form" action="/test/">
<input type="hidden" name="csrfmiddlewaretoken" value="test-token">
<!-- Subject Field -->
<div class="mb-3">
<label for="id_subject" class="form-label fw-bold">
Subject
</label>
<input type="text" class="form-control" id="id_subject" name="subject">
</div>
<!-- Recipients Field -->
<div class="mb-3">
<label class="form-label fw-bold">
Recipients
</label>
<div class="border rounded p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" name="recipients" value="user1@example.com" id="recipient1">
<label class="form-check-label" for="recipient1">user1@example.com</label>
</div>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" name="recipients" value="user2@example.com" id="recipient2">
<label class="form-check-label" for="recipient2">user2@example.com</label>
</div>
</div>
</div>
<!-- Message Field -->
<div class="mb-3">
<label for="id_message" class="form-label fw-bold">
Message
</label>
<textarea class="form-control" id="id_message" name="message" rows="5"></textarea>
</div>
<!-- Options Checkboxes -->
<div class="mb-4">
<div class="row">
<div class="col-md-6">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="include_candidate_info" id="id_include_candidate_info">
<label class="form-check-label" for="id_include_candidate_info">
Include candidate information
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="include_meeting_details" id="id_include_meeting_details">
<label class="form-check-label" for="id_include_meeting_details">
Include meeting details
</label>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
<i class="fas fa-info-circle me-1"></i>
Email will be sent to all selected recipients
</div>
<div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>
Cancel
</button>
<button type="submit" class="btn btn-primary" id="send-email-btn">
<i class="fas fa-paper-plane me-1"></i>
Send Email
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="email-loading-overlay" class="d-none">
<div class="d-flex justify-content-center align-items-center" style="min-height: 200px;">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="mt-2">
Sending email...
</div>
</div>
</div>
</div>
<!-- Success/Error Messages Container -->
<div id="email-messages-container"></div>
</div>
<!-- Test Results -->
<div class="mt-4">
<h3>Test Results</h3>
<div id="test-results"></div>
</div>
</div>
<style>
.card {
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-radius: 8px;
}
.card-header {
border-radius: 8px 8px 0 0 !important;
border-bottom: none;
}
.form-control:focus {
border-color: #00636e;
box-shadow: 0 0 0 0.2rem rgba(0,99,110,0.25);
}
.btn-primary {
background-color: #00636e;
border-color: #00636e;
}
.btn-primary:hover {
background-color: #004a53;
border-color: #004a53;
}
.form-check-input:checked {
background-color: #00636e;
border-color: #00636e;
}
.border {
border-color: #dee2e6 !important;
}
.bg-light {
background-color: #f8f9fa !important;
}
.text-danger {
color: #dc3545 !important;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('email-compose-form');
const sendBtn = document.getElementById('send-email-btn');
const loadingOverlay = document.getElementById('email-loading-overlay');
const messagesContainer = document.getElementById('email-messages-container');
const testResults = document.getElementById('test-results');
// Test results tracking
let tests = [];
function addTestResult(testName, passed, message) {
tests.push({ name: testName, passed, message });
updateTestResults();
}
function updateTestResults() {
const passedTests = tests.filter(t => t.passed).length;
const totalTests = tests.length;
testResults.innerHTML = `
<div class="alert alert-info">
<strong>Tests: ${passedTests}/${totalTests} passed</strong>
</div>
<ul class="list-group">
${tests.map(test => `
<li class="list-group-item ${test.passed ? 'list-group-item-success' : 'list-group-item-danger'}">
<i class="fas fa-${test.passed ? 'check' : 'times'} me-2"></i>
<strong>${test.name}:</strong> ${test.message}
</li>
`).join('')}
</ul>
`;
}
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
addTestResult('Form Submit Handler', true, 'Form submission intercepted successfully');
// Show loading state
if (sendBtn) {
sendBtn.disabled = true;
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Sending...';
addTestResult('Loading State', true, 'Button loading state activated');
}
if (loadingOverlay) {
loadingOverlay.classList.remove('d-none');
addTestResult('Loading Overlay', true, 'Loading overlay displayed');
}
// Clear previous messages
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
// Mock form submission
setTimeout(() => {
// Hide loading state
if (sendBtn) {
sendBtn.disabled = false;
sendBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> Send Email';
}
if (loadingOverlay) {
loadingOverlay.classList.add('d-none');
}
// Show success message
showMessage('Email sent successfully!', 'success');
addTestResult('Success Message', true, 'Success message displayed');
}, 2000);
});
}
function showMessage(message, type) {
if (!messagesContainer) return;
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle';
const messageHtml = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
<i class="fas ${icon} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
messagesContainer.innerHTML = messageHtml;
// Auto-hide success messages after 5 seconds
if (type === 'success') {
setTimeout(() => {
const alert = messagesContainer.querySelector('.alert');
if (alert) {
const bsAlert = bootstrap.Alert(alert);
bsAlert.close();
addTestResult('Auto-hide Message', true, 'Message auto-hidden after 5 seconds');
}
}, 5000);
}
}
// Form validation
function validateForm() {
let isValid = true;
const subject = form.querySelector('#id_subject');
const message = form.querySelector('#id_message');
const recipients = form.querySelectorAll('input[name="recipients"]:checked');
// Clear previous validation states
form.querySelectorAll('.is-invalid').forEach(field => {
field.classList.remove('is-invalid');
});
form.querySelectorAll('.invalid-feedback').forEach(feedback => {
feedback.remove();
});
// Validate subject
if (!subject || !subject.value.trim()) {
showFieldError(subject, 'Subject is required');
isValid = false;
addTestResult('Subject Validation', false, 'Subject validation triggered - field empty');
} else {
addTestResult('Subject Validation', true, 'Subject validation passed');
}
// Validate message
if (!message || !message.value.trim()) {
showFieldError(message, 'Message is required');
isValid = false;
addTestResult('Message Validation', false, 'Message validation triggered - field empty');
} else {
addTestResult('Message Validation', true, 'Message validation passed');
}
// Validate recipients
if (recipients.length === 0) {
const recipientsContainer = form.querySelector('.border.rounded.p-3.bg-light');
if (recipientsContainer) {
recipientsContainer.classList.add('border-danger');
showFieldError(recipientsContainer, 'Please select at least one recipient');
}
isValid = false;
addTestResult('Recipients Validation', false, 'Recipients validation triggered - none selected');
} else {
addTestResult('Recipients Validation', true, `Recipients validation passed - ${recipients.length} selected`);
}
return isValid;
}
function showFieldError(field, message) {
if (!field) return;
field.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = message;
if (field.classList.contains('border')) {
// For container elements (like recipients)
field.parentNode.appendChild(feedback);
} else {
// For form fields
field.parentNode.appendChild(feedback);
}
}
// Character counter for message field
function setupCharacterCounter() {
const messageField = form.querySelector('#id_message');
if (!messageField) return;
const counter = document.createElement('div');
counter.className = 'text-muted small mt-1';
counter.id = 'message-counter';
messageField.parentNode.appendChild(counter);
function updateCounter() {
const length = messageField.value.length;
const maxLength = 5000; // Adjust as needed
counter.textContent = `${length} / ${maxLength} characters`;
if (length > maxLength * 0.9) {
counter.classList.add('text-warning');
counter.classList.remove('text-muted');
} else {
counter.classList.remove('text-warning');
counter.classList.add('text-muted');
}
}
messageField.addEventListener('input', updateCounter);
updateCounter(); // Initial count
addTestResult('Character Counter', true, 'Character counter initialized');
}
// Auto-save functionality
let autoSaveTimer;
function setupAutoSave() {
const subject = form.querySelector('#id_subject');
const message = form.querySelector('#id_message');
if (!subject || !message) return;
function saveDraft() {
const draftData = {
subject: subject.value,
message: message.value,
recipients: Array.from(form.querySelectorAll('input[name="recipients"]:checked')).map(cb => cb.value),
include_candidate_info: form.querySelector('#id_include_candidate_info').checked,
include_meeting_details: form.querySelector('#id_include_meeting_details').checked
};
localStorage.setItem('email_draft_test', JSON.stringify(draftData));
addTestResult('Auto-save', true, 'Draft saved to localStorage');
}
function autoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(saveDraft, 2000); // Save after 2 seconds of inactivity
}
[subject, message].forEach(field => {
field.addEventListener('input', autoSave);
});
form.addEventListener('change', autoSave);
addTestResult('Auto-save Setup', true, 'Auto-save functionality initialized');
}
function loadDraft() {
const draftData = localStorage.getItem('email_draft_test');
if (!draftData) return;
try {
const draft = JSON.parse(draftData);
const subject = form.querySelector('#id_subject');
const message = form.querySelector('#id_message');
if (subject && draft.subject) subject.value = draft.subject;
if (message && draft.message) message.value = draft.message;
// Restore recipients
if (draft.recipients) {
form.querySelectorAll('input[name="recipients"]').forEach(cb => {
cb.checked = draft.recipients.includes(cb.value);
});
}
// Restore checkboxes
if (draft.include_candidate_info) {
form.querySelector('#id_include_candidate_info').checked = draft.include_candidate_info;
}
if (draft.include_meeting_details) {
form.querySelector('#id_include_meeting_details').checked = draft.include_meeting_details;
}
addTestResult('Draft Loading', true, 'Draft loaded from localStorage');
showMessage('Draft restored from local storage', 'success');
} catch (e) {
console.error('Error loading draft:', e);
addTestResult('Draft Loading', false, 'Error loading draft: ' + e.message);
}
}
function clearDraft() {
localStorage.removeItem('email_draft_test');
}
// Initialize form enhancements
setupCharacterCounter();
setupAutoSave();
// Load draft on page load
setTimeout(loadDraft, 100);
// Clear draft on successful submission
const originalSubmitHandler = form.onsubmit;
form.addEventListener('submit', function(e) {
const isValid = validateForm();
if (!isValid) {
e.preventDefault();
e.stopPropagation();
return false;
}
// Clear draft on successful submission
setTimeout(clearDraft, 2000);
});
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl/Cmd + Enter to submit
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'TEXTAREA' || activeElement.tagName === 'INPUT')) {
form.dispatchEvent(new Event('submit'));
addTestResult('Keyboard Shortcut', true, 'Ctrl+Enter shortcut triggered');
}
}
// Escape to cancel/close modal
if (e.key === 'Escape') {
addTestResult('Keyboard Shortcut', true, 'Escape key pressed');
}
});
// Test validation with empty form
setTimeout(() => {
addTestResult('Initial Validation Test', validateForm() === false, 'Empty form correctly rejected');
}, 500);
console.log('Email compose form initialized');
addTestResult('Initialization', true, 'Email compose form JavaScript initialized successfully');
});
</script>
</body>
</html>

176
test_html_email_template.py Normal file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env python
"""
Test script for HTML email template functionality
"""
import os
import sys
import django
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
django.setup()
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from recruitment.models import Candidate, JobPosting
from recruitment.email_service import send_interview_invitation_email
def test_html_template():
"""Test the HTML email template rendering"""
print("Testing HTML email template...")
# Create test context
context = {
'candidate_name': 'John Doe',
'candidate_email': 'john.doe@example.com',
'candidate_phone': '+966 50 123 4567',
'job_title': 'Senior Software Developer',
'department': 'Information Technology',
'company_name': 'Norah University',
'meeting_topic': 'Interview for Senior Software Developer',
'meeting_date_time': 'November 15, 2025 at 2:00 PM',
'meeting_duration': '60 minutes',
'join_url': 'https://zoom.us/j/123456789',
}
try:
# Test template rendering
html_content = render_to_string('emails/interview_invitation.html', context)
plain_content = strip_tags(html_content)
print("✅ HTML template rendered successfully!")
print(f"HTML content length: {len(html_content)} characters")
print(f"Plain text length: {len(plain_content)} characters")
# Save rendered HTML to file for inspection
with open('test_interview_email.html', 'w', encoding='utf-8') as f:
f.write(html_content)
print("✅ HTML content saved to 'test_interview_email.html'")
# Save plain text to file for inspection
with open('test_interview_email.txt', 'w', encoding='utf-8') as f:
f.write(plain_content)
print("✅ Plain text content saved to 'test_interview_email.txt'")
return True
except Exception as e:
print(f"❌ Error rendering template: {e}")
return False
def test_email_service_function():
"""Test the email service function with mock data"""
print("\nTesting email service function...")
try:
# Get a real candidate and job for testing
candidate = Candidate.objects.first()
job = JobPosting.objects.first()
if not candidate:
print("❌ No candidates found in database")
return False
if not job:
print("❌ No jobs found in database")
return False
print(f"Using candidate: {candidate.name}")
print(f"Using job: {job.title}")
# Test meeting details
meeting_details = {
'topic': f'Interview for {job.title}',
'date_time': 'November 15, 2025 at 2:00 PM',
'duration': '60 minutes',
'join_url': 'https://zoom.us/j/test123456',
}
# Test the email function (without actually sending)
result = send_interview_invitation_email(
candidate=candidate,
job=job,
meeting_details=meeting_details,
recipient_list=['test@example.com']
)
if result['success']:
print("✅ Email service function executed successfully!")
print(f"Recipients: {result.get('recipients_count', 'N/A')}")
print(f"Message: {result.get('message', 'N/A')}")
else:
print(f"❌ Email service function failed: {result.get('error', 'Unknown error')}")
return result['success']
except Exception as e:
print(f"❌ Error testing email service: {e}")
return False
def test_template_variables():
"""Test all template variables"""
print("\nTesting template variables...")
# Test with minimal data
minimal_context = {
'candidate_name': 'Test Candidate',
'candidate_email': 'test@example.com',
'job_title': 'Test Position',
}
try:
html_content = render_to_string('emails/interview_invitation.html', minimal_context)
print("✅ Template works with minimal data")
# Check for required variables
required_vars = ['candidate_name', 'candidate_email', 'job_title']
missing_vars = []
for var in required_vars:
if f'{{ {var} }}' in html_content:
missing_vars.append(var)
if missing_vars:
print(f"⚠️ Missing variables: {missing_vars}")
else:
print("✅ All required variables are present")
return True
except Exception as e:
print(f"❌ Error with minimal data: {e}")
return False
def main():
"""Run all tests"""
print("🧪 Testing HTML Email Template System")
print("=" * 50)
# Test 1: Template rendering
test1_passed = test_html_template()
# Test 2: Template variables
test2_passed = test_template_variables()
# Test 3: Email service function
test3_passed = test_email_service_function()
# Summary
print("\n" + "=" * 50)
print("📊 TEST SUMMARY")
print(f"Template Rendering: {'✅ PASS' if test1_passed else '❌ FAIL'}")
print(f"Template Variables: {'✅ PASS' if test2_passed else '❌ FAIL'}")
print(f"Email Service: {'✅ PASS' if test3_passed else '❌ FAIL'}")
overall_success = test1_passed and test2_passed and test3_passed
print(f"\nOverall Result: {'✅ ALL TESTS PASSED' if overall_success else '❌ SOME TESTS FAILED'}")
if overall_success:
print("\n🎉 HTML email template system is ready!")
print("You can now send professional interview invitations using the new template.")
else:
print("\n🔧 Please fix the issues before using the template system.")
if __name__ == '__main__':
main()

139
test_interview_email.html Normal file
View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Interview Invitation</title>
<style>
/* Basic reset and typography */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333333;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
/* Container for the main content */
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Header styling */
.header {
text-align: center;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.header h1 {
color: #007bff;
font-size: 24px;
margin: 0;
}
/* Section headings */
.section-header {
color: #007bff;
font-size: 18px;
margin-top: 25px;
margin-bottom: 10px;
border-left: 4px solid #007bff;
padding-left: 10px;
}
/* Key detail rows */
.detail-row {
margin-bottom: 10px;
}
.detail-row strong {
display: inline-block;
width: 120px;
color: #555555;
}
/* Button style for the Join URL */
.button {
display: block;
width: 80%;
margin: 25px auto;
padding: 12px 0;
background-color: #28a745; /* Success/Go color */
color: #ffffff;
text-align: center;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
}
/* Footer/closing section */
.footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px dashed #cccccc;
text-align: center;
font-size: 14px;
color: #777777;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Interview Confirmation</h1>
</div>
<p>Dear <strong>John Doe</strong>,</p>
<p>Thank you for your interest in the position. We are pleased to invite you to a virtual interview. Please find the details below.</p>
<h2 class="section-header">Interview Details</h2>
<div class="detail-row">
<strong>Topic:</strong> Interview for Senior Software Developer
</div>
<div class="detail-row">
<strong>Date & Time:</strong> <strong>November 15, 2025 at 2:00 PM</strong>
</div>
<div class="detail-row">
<strong>Duration:</strong> 60 minutes
</div>
<a href="https://zoom.us/j/123456789" class="button" target="_blank">
Join Interview Now
</a>
<p style="text-align: center; font-size: 14px; color: #777;">Please click the button above to join the meeting at the scheduled time.</p>
<h2 class="section-header">Your Information</h2>
<div class="detail-row">
<strong>Name:</strong> John Doe
</div>
<div class="detail-row">
<strong>Email:</strong> john.doe@example.com
</div>
<div class="detail-row">
<strong>Phone:</strong> +966 50 123 4567
</div>
<h2 class="section-header">Position Details</h2>
<div class="detail-row">
<strong>Position:</strong> Senior Software Developer
</div>
<div class="detail-row">
<strong>Department:</strong> Information Technology
</div>
<div class="footer">
<p>We look forward to speaking with you.</p>
<p>If you have any questions, please reply to this email.</p>
<p>Best regards,<br>The Norah University Team</p>
</div>
</div>
</body>
</html>

139
test_interview_email.txt Normal file
View File

@ -0,0 +1,139 @@
Interview Invitation
/* Basic reset and typography */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333333;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
/* Container for the main content */
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Header styling */
.header {
text-align: center;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.header h1 {
color: #007bff;
font-size: 24px;
margin: 0;
}
/* Section headings */
.section-header {
color: #007bff;
font-size: 18px;
margin-top: 25px;
margin-bottom: 10px;
border-left: 4px solid #007bff;
padding-left: 10px;
}
/* Key detail rows */
.detail-row {
margin-bottom: 10px;
}
.detail-row strong {
display: inline-block;
width: 120px;
color: #555555;
}
/* Button style for the Join URL */
.button {
display: block;
width: 80%;
margin: 25px auto;
padding: 12px 0;
background-color: #28a745; /* Success/Go color */
color: #ffffff;
text-align: center;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
}
/* Footer/closing section */
.footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px dashed #cccccc;
text-align: center;
font-size: 14px;
color: #777777;
}
Interview Confirmation
Dear John Doe,
Thank you for your interest in the position. We are pleased to invite you to a virtual interview. Please find the details below.
Interview Details
Topic: Interview for Senior Software Developer
Date & Time: November 15, 2025 at 2:00 PM
Duration: 60 minutes
Join Interview Now
Please click the button above to join the meeting at the scheduled time.
Your Information
Name: John Doe
Email: john.doe@example.com
Phone: +966 50 123 4567
Position Details
Position: Senior Software Developer
Department: Information Technology
We look forward to speaking with you.
If you have any questions, please reply to this email.
Best regards,The Norah University Team

239
test_simple_email.py Normal file
View File

@ -0,0 +1,239 @@
#!/usr/bin/env python
"""
Simple test script for basic email functionality without attachments
"""
import os
import sys
import django
from django.conf import settings
# Configure Django settings BEFORE importing any Django modules
if not settings.configured:
settings.configure(
DEBUG=True,
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
},
USE_TZ=True,
SECRET_KEY='test-secret-key',
INSTALLED_APPS=[
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'recruitment',
],
EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend',
)
# Setup Django
django.setup()
# Now import Django modules
from django.test import TestCase, Client
from django.test import RequestFactory
from django.contrib.auth.models import User
from recruitment.email_service import send_bulk_email
from recruitment.forms import CandidateEmailForm
from recruitment.models import JobPosting, Candidate, Participants
def setup_test_data():
"""Create test data for email testing"""
# Create test user (get or create to avoid duplicates)
user, created = User.objects.get_or_create(
username='testuser',
defaults={
'email': 'test@example.com',
'first_name': 'Test',
'last_name': 'User'
}
)
# Create test job
from datetime import datetime, timedelta
job = JobPosting.objects.create(
title='Test Job Position',
description='This is a test job for email testing.',
status='ACTIVE',
internal_job_id='TEST-001',
application_deadline=datetime.now() + timedelta(days=30)
)
# Create test candidate
candidate = Candidate.objects.create(
first_name='John',
last_name='Doe',
email='john.doe@example.com',
phone='+1234567890',
address='123 Test Street',
job=job,
stage='Interview'
)
# Create test participants
participant1 = Participants.objects.create(
name='Alice Smith',
email='alice@example.com',
phone='+1234567891',
designation='Interviewer'
)
participant2 = Participants.objects.create(
name='Bob Johnson',
email='bob@example.com',
phone='+1234567892',
designation='Hiring Manager'
)
# Add participants to job
job.participants.add(participant1, participant2)
return user, job, candidate, [participant1, participant2]
def test_email_service_basic():
"""Test the email service with basic functionality"""
print("Testing basic email service...")
try:
# Test email service without attachments
result = send_bulk_email(
subject='Test Basic Email',
message='This is a test email without attachments.',
recipient_list=['recipient1@example.com', 'recipient2@example.com']
)
print(f"Email service result: {result}")
print("✓ Basic email service test passed")
return True
except Exception as e:
print(f"✗ Basic email service test failed: {e}")
return False
def test_candidate_email_form_basic():
"""Test the CandidateEmailForm without attachments"""
print("\nTesting CandidateEmailForm without attachments...")
user, job, candidate, participants = setup_test_data()
form_data = {
'subject': 'Test Subject',
'message': 'Test body content',
'recipients': [f'participant_{p.id}' for p in participants],
'include_candidate_info': True,
'include_meeting_details': True,
}
try:
form = CandidateEmailForm(data=form_data, job=job, candidate=candidate)
if form.is_valid():
print("✓ Form validation passed")
print(f"Form cleaned data keys: {list(form.cleaned_data.keys())}")
# Test getting email addresses
email_addresses = form.get_email_addresses()
print(f"Email addresses: {email_addresses}")
# Test getting formatted message
formatted_message = form.get_formatted_message()
print(f"Formatted message length: {len(formatted_message)} characters")
return True
else:
print(f"✗ Form validation failed: {form.errors}")
return False
except Exception as e:
print(f"✗ Form test failed: {e}")
return False
def test_email_sending_workflow():
"""Test the complete email sending workflow"""
print("\nTesting complete email sending workflow...")
user, job, candidate, participants = setup_test_data()
form_data = {
'subject': 'Interview Update: John Doe - Test Job Position',
'message': 'Please find the interview update below.',
'recipients': [f'participant_{p.id}' for p in participants],
'include_candidate_info': True,
'include_meeting_details': True,
}
try:
# Create and validate form
form = CandidateEmailForm(data=form_data, job=job, candidate=candidate)
if not form.is_valid():
print(f"✗ Form validation failed: {form.errors}")
return False
# Get email data
subject = form.cleaned_data['subject']
message = form.get_formatted_message()
recipient_emails = form.get_email_addresses()
print(f"Subject: {subject}")
print(f"Recipients: {recipient_emails}")
print(f"Message preview: {message[:200]}...")
# Send email using service
result = send_bulk_email(
subject=subject,
message=message,
recipient_list=recipient_emails
)
print(f"Email sending result: {result}")
print("✓ Complete email workflow test passed")
return True
except Exception as e:
print(f"✗ Email workflow test failed: {e}")
return False
def main():
"""Run all simple email tests"""
print("=" * 60)
print("SIMPLE EMAIL FUNCTIONALITY TESTS")
print("=" * 60)
# Initialize Django
django.setup()
# Create tables
from django.core.management import execute_from_command_line
execute_from_command_line(['manage.py', 'migrate', '--run-syncdb'])
results = []
# Run tests
results.append(test_email_service_basic())
results.append(test_candidate_email_form_basic())
results.append(test_email_sending_workflow())
# Summary
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
passed = sum(results)
total = len(results)
print(f"Tests passed: {passed}/{total}")
if passed == total:
print("🎉 All simple email tests passed!")
return True
else:
print("❌ Some simple email tests failed!")
return False
if __name__ == '__main__':
success = main()
exit(0 if success else 1)