Merge pull request 'interview result added and issue resolved' (#113) from frontend into main

Reviewed-on: #113
This commit is contained in:
ismail 2025-12-15 16:47:07 +03:00
commit 58f2489676
14 changed files with 485 additions and 300 deletions

6
.env
View File

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

View File

@ -208,7 +208,9 @@ ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"} ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"}
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" MAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_HOST = "10.10.1.110"
EMAIL_PORT = 2225
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI") # EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI")
@ -217,10 +219,10 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# EMAIL_HOST_USER = "MS_lhygCJ@test-65qngkd8nx3lwr12.mlsender.net" # EMAIL_HOST_USER = "MS_lhygCJ@test-65qngkd8nx3lwr12.mlsender.net"
# EMAIL_HOST_PASSWORD = "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI" # EMAIL_HOST_PASSWORD = "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI"
# EMAIL_USE_TLS = True # EMAIL_USE_TLS = True
EMAIL_HOST = 'sandbox.smtp.mailtrap.io' # EMAIL_HOST = 'sandbox.smtp.mailtrap.io'
EMAIL_HOST_USER = '38e5179debe69a' # EMAIL_HOST_USER = '38e5179debe69a'
EMAIL_HOST_PASSWORD = 'ffa75647d01ecb' # EMAIL_HOST_PASSWORD = 'ffa75647d01ecb'
EMAIL_PORT = '2525' # EMAIL_PORT = '2525'
# Crispy Forms Configuration # Crispy Forms Configuration

View File

@ -1496,6 +1496,7 @@ class MessageForm(forms.ModelForm):
self.helper.form_class = "g-3" self.helper.form_class = "g-3"
self._filter_recipient_field() self._filter_recipient_field()
self._filter_job_field()
self.helper.layout = Layout( self.helper.layout = Layout(
Row( Row(
@ -1516,6 +1517,7 @@ class MessageForm(forms.ModelForm):
"""Filter job options based on user type""" """Filter job options based on user type"""
if self.user.user_type == "agency": if self.user.user_type == "agency":
print("jhjkshfjksd")
job_assignments =AgencyJobAssignment.objects.filter( job_assignments =AgencyJobAssignment.objects.filter(
agency__user=self.user, agency__user=self.user,
@ -1528,11 +1530,18 @@ class MessageForm(forms.ModelForm):
print("Agency user job queryset:", self.fields["job"].queryset) print("Agency user job queryset:", self.fields["job"].queryset)
elif self.user.user_type == "candidate": elif self.user.user_type == "candidate":
print("sjhdakjhsdkjashkdjhskd")
# Candidates can only see jobs they applied for # Candidates can only see jobs they applied for
person=self.user.person_profile
print(person)
applications=person.applications.all()
print(applications)
self.fields["job"].queryset = JobPosting.objects.filter( self.fields["job"].queryset = JobPosting.objects.filter(
applications__person=self.user.person_profile, applications__in=applications,
).distinct().order_by("-created_at") ).distinct().order_by("-created_at")
else: else:
print("shhadjkhkd")
# Staff can see all jobs # Staff can see all jobs
self.fields["job"].queryset = JobPosting.objects.filter( self.fields["job"].queryset = JobPosting.objects.filter(
status="ACTIVE" status="ACTIVE"
@ -2145,3 +2154,22 @@ KAAUH Hiring Team
self.fields['message'].initial = initial_message self.fields['message'].initial = initial_message
class InterviewResultForm(forms.ModelForm):
class Meta:
model = Interview
fields = ['interview_result', 'result_comments']
widgets = {
'interview_result': forms.Select(attrs={
'class': 'form-select', # Standard Bootstrap class
'required': 'required'
}),
'result_comments': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter setting value',
'required': True
}),
}

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-12-15 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_alter_source_name_alter_source_source_type'),
]
operations = [
migrations.AddField(
model_name='interview',
name='interview_result',
field=models.CharField(blank=True, choices=[('passed', 'Passed'), ('failed', 'Failed'), ('on_hold', 'ON Hold')], default='on_hold', max_length=10, null=True, verbose_name='Interview Result'),
),
migrations.AddField(
model_name='interview',
name='result_comments',
field=models.TextField(blank=True, null=True),
),
]

View File

@ -1122,6 +1122,11 @@ class Interview(Base):
STARTED = "started", _("Started") STARTED = "started", _("Started")
ENDED = "ended", _("Ended") ENDED = "ended", _("Ended")
CANCELLED = "cancelled", _("Cancelled") CANCELLED = "cancelled", _("Cancelled")
class InterviewResult(models.TextChoices):
PASSED="passed",_("Passed")
FAILED="failed",_("Failed")
ON_HOLD="on_hold",_("ON Hold")
location_type = models.CharField( location_type = models.CharField(
max_length=10, max_length=10,
@ -1129,6 +1134,18 @@ class Interview(Base):
verbose_name=_("Location Type"), verbose_name=_("Location Type"),
db_index=True, db_index=True,
) )
interview_result=models.CharField(
max_length=10,
choices=InterviewResult.choices,
verbose_name=_("Interview Result"),
null=True,
blank=True,
default='on_hold'
)
result_comments=models.TextField(
null=True,
blank=True
)
# Common fields # Common fields
topic = models.CharField( topic = models.CharField(

View File

@ -3,6 +3,7 @@ from django.core.mail import send_mail, EmailMessage
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.conf import settings # To access EMAIL_HOST_USER, etc. from django.conf import settings # To access EMAIL_HOST_USER, etc.
from recruitment.models import Message
UserModel = get_user_model() UserModel = get_user_model()
User = UserModel # Type alias for clarity User = UserModel # Type alias for clarity
@ -17,28 +18,37 @@ class EmailService:
subject: str, subject: str,
body: str, body: str,
recipient_list: List[str], recipient_list: List[str],
context:dict,
from_email: str = settings.DEFAULT_FROM_EMAIL, from_email: str = settings.DEFAULT_FROM_EMAIL,
html_content: Union[str, None] = None html_content: Union[str, None] = None,
) -> int: ) -> int:
""" """
Internal method to handle the actual sending using Django's email backend. Internal method to handle the actual sending using Django's email backend.
""" """
try: try:
# Using EmailMessage for more control (e.g., HTML content) # Using EmailMessage for more control (e.g., HTML content)
email = EmailMessage(
subject=subject, for recipient in recipient_list:
body=body, email = EmailMessage(
from_email=from_email, subject=subject,
to=recipient_list, body=body,
) from_email=from_email,
to=[recipient],
)
if html_content: if html_content:
email.content_subtype = "html" # Main content is HTML email.content_subtype = "html" # Main content is HTML
email.body = html_content # Overwrite body with HTML email.body = html_content # Overwrite body with HTML
# Returns the number of successfully sent emails (usually 1 or the count of recipients) # Returns the number of successfully sent emails (usually 1 or the count of recipients)
sent_count = email.send(fail_silently=False) result=email.send(fail_silently=False)
return sent_count recipient_user=User.objects.filter(email=recipient).first()
if result and recipient_user and not context["message_created"]:
Message.objects.create(sender=context['sender_user'],recipient=recipient_user,job=context['job'],subject=subject,content=context['email_message'],message_type='DIRECT',is_read=False)
return len(recipient_list)
except Exception as e: except Exception as e:
# Log the error (in a real app, use Django's logger) # Log the error (in a real app, use Django's logger)
@ -46,34 +56,34 @@ class EmailService:
return 0 return 0
def send_single_email( # def send_single_email(
self, # self,
user: User, # user: User,
subject: str, # subject: str,
template_name: str, # template_name: str,
context: dict, # context: dict,
from_email: str = settings.DEFAULT_FROM_EMAIL # from_email: str = settings.DEFAULT_FROM_EMAIL
) -> int: # ) -> int:
""" # """
Sends a single, template-based email to one user. # Sends a single, template-based email to one user.
""" # """
recipient_list = [user.email] # recipient_list = [user.email]
# 1. Render content from template # # 1. Render content from template
html_content = render_to_string(template_name, context) # html_content = render_to_string(template_name, context)
# You can optionally render a plain text version as well: # # You can optionally render a plain text version as well:
# text_content = strip_tags(html_content) # # text_content = strip_tags(html_content)
# 2. Call internal sender # # 2. Call internal sender
return self._send_email_internal( # return self._send_email_internal(
subject=subject, # subject=subject,
body="", # Can be empty if html_content is provided # body="", # Can be empty if html_content is provided
recipient_list=recipient_list, # recipient_list=recipient_list,
from_email=from_email, # from_email=from_email,
html_content=html_content # html_content=html_content
) # )
def send_bulk_email( def send_email_service(
self, self,
recipient_emails: List[str], recipient_emails: List[str],
subject: str, subject: str,
@ -98,8 +108,10 @@ class EmailService:
subject=subject, subject=subject,
body="", body="",
recipient_list=recipient_emails, recipient_list=recipient_emails,
context=context,
from_email=from_email, from_email=from_email,
html_content=html_content html_content=html_content,
) )
# Return the count of recipients if successful, or 0 if failure # Return the count of recipients if successful, or 0 if failure

View File

@ -1534,7 +1534,7 @@ def send_job_closed_notification(job_id):
) )
def send_bulk_email_task( def send_email_task(
recipient_emails, recipient_emails,
subject: str, subject: str,
template_name: str, template_name: str,
@ -1551,7 +1551,7 @@ def send_bulk_email_task(
service = EmailService() service = EmailService()
# Execute the bulk sending method # Execute the bulk sending method
processed_count = service.send_bulk_email( processed_count = service.send_email_service(
recipient_emails=recipient_emails, recipient_emails=recipient_emails,
subject=subject, subject=subject,
template_name=template_name, template_name=template_name,
@ -1565,33 +1565,33 @@ def send_bulk_email_task(
"message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}." "message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}."
}) })
def send_single_email_task( # def send_single_email_task(
recipient_emails, # recipient_emails,
subject: str, # subject: str,
template_name: str, # template_name: str,
context: dict, # context: dict,
) -> str: # ) -> str:
""" # """
Django-Q task to send a bulk email asynchronously. # Django-Q task to send a bulk email asynchronously.
""" # """
from .services.email_service import EmailService # from .services.email_service import EmailService
if not recipient_emails: # if not recipient_emails:
return json.dumps({"status": "error", "message": "No recipients provided."}) # return json.dumps({"status": "error", "message": "No recipients provided."})
service = EmailService() # service = EmailService()
# Execute the bulk sending method # # Execute the bulk sending method
processed_count = service.send_bulk_email( # processed_count = service.send_bulk_email(
recipient_emails=recipient_emails, # recipient_emails=recipient_emails,
subject=subject, # subject=subject,
template_name=template_name, # template_name=template_name,
context=context, # context=context,
) # )
# The return value is stored in the result object for monitoring # # The return value is stored in the result object for monitoring
return json.dumps({ # return json.dumps({
"status": "success", # "status": "success",
"count": processed_count, # "count": processed_count,
"message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}." # "message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}."
}) # })

View File

@ -83,6 +83,8 @@ urlpatterns = [
path("interviews/", views.interview_list, name="interview_list"), path("interviews/", views.interview_list, name="interview_list"),
path("interviews/<slug:slug>/", views.interview_detail, name="interview_detail"), path("interviews/<slug:slug>/", views.interview_detail, name="interview_detail"),
path("interviews/<slug:slug>/update_interview_status", views.update_interview_status, name="update_interview_status"), path("interviews/<slug:slug>/update_interview_status", views.update_interview_status, name="update_interview_status"),
path("interviews/<slug:slug>/update_interview_result", views.update_interview_result, name="update_interview_result"),
path("interviews/<slug:slug>/cancel_interview_for_application", views.cancel_interview_for_application, name="cancel_interview_for_application"), path("interviews/<slug:slug>/cancel_interview_for_application", views.cancel_interview_for_application, name="cancel_interview_for_application"),
path("interview/<slug:slug>/interview-email/",views.send_interview_email,name="send_interview_email"), path("interview/<slug:slug>/interview-email/",views.send_interview_email,name="send_interview_email"),

View File

@ -94,6 +94,7 @@ from .forms import (
InterviewCancelForm, InterviewCancelForm,
InterviewEmailForm, InterviewEmailForm,
ApplicationStageForm, ApplicationStageForm,
InterviewResultForm
) )
from .utils import generate_random_password from .utils import generate_random_password
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -1764,6 +1765,7 @@ def _handle_confirm_schedule(request, slug, job):
for i, application in enumerate(applications): for i, application in enumerate(applications):
if i < len(available_slots): if i < len(available_slots):
slot = available_slots[i] slot = available_slots[i]
# schedule=ScheduledInterview.objects.create(application=application,job=job)
async_task( async_task(
"recruitment.tasks.create_interview_and_meeting", "recruitment.tasks.create_interview_and_meeting",
application.pk, application.pk,
@ -2141,6 +2143,7 @@ def reschedule_meeting_for_application(request, slug):
if request.method == "POST": if request.method == "POST":
if interview.location_type == "Remote": if interview.location_type == "Remote":
form = ScheduledInterviewForm(request.POST) form = ScheduledInterviewForm(request.POST)
else: else:
form = OnsiteScheduleInterviewUpdateForm(request.POST) form = OnsiteScheduleInterviewUpdateForm(request.POST)
@ -3055,6 +3058,8 @@ def applicant_portal_dashboard(request):
# Get candidate's documents using the Person documents property # Get candidate's documents using the Person documents property
documents = applicant.documents.order_by("-created_at") documents = applicant.documents.order_by("-created_at")
print(documents)
# Add password change form for modal # Add password change form for modal
password_form = PasswordResetForm() password_form = PasswordResetForm()
@ -3653,10 +3658,8 @@ def message_detail(request, message_id):
@login_required @login_required
def message_create(request): def message_create(request):
"""Create a new message""" """Create a new message"""
from .email_service import EmailService from django.conf import settings
from .services.email_service import UnifiedEmailService from django_q.tasks import async_task
from .dto.email_dto import EmailConfig, BulkEmailConfig, EmailPriority
if request.method == "POST": if request.method == "POST":
form = MessageForm(request.user, request.POST) form = MessageForm(request.user, request.POST)
@ -3679,22 +3682,25 @@ def message_create(request):
# from .services.email_service import UnifiedEmailService # from .services.email_service import UnifiedEmailService
# from .dto.email_dto import EmailConfig, EmailPriority # from .dto.email_dto import EmailConfig, EmailPriority
service = UnifiedEmailService()
# Create email configuration email_addresses = [message.recipient.email]
config = EmailConfig( subject=message.subject
to_email=message.recipient.email,
subject=message.subject, email_result=async_task(
html_content=body, "recruitment.tasks.send_email_task",
attachments=None, email_addresses,
sender=request.user subject,
if request and hasattr(request, "user") # message,
else None, "emails/email_template.html",
priority=EmailPriority.NORMAL, {
"email_message": body,
"logo_url": settings.STATIC_URL + "image/kaauh.png",
"message_created":True
},
) )
# Send email using unified service # Send email using unified service
email_result = service.send_email(config)
if email_result: if email_result:
messages.success( messages.success(
request, "Message sent successfully via email!" request, "Message sent successfully via email!"
@ -3749,7 +3755,7 @@ def message_create(request):
and "HX-Request" in request.headers and "HX-Request" in request.headers
and request.user.user_type in ["candidate", "agency"] and request.user.user_type in ["candidate", "agency"]
): ):
print()
job_id = request.GET.get("job") job_id = request.GET.get("job")
if job_id: if job_id:
job = get_object_or_404(JobPosting, id=job_id) job = get_object_or_404(JobPosting, id=job_id)
@ -4251,7 +4257,7 @@ def cancel_interview_for_application(request, slug):
Handles POST request to cancel an interview, setting the status Handles POST request to cancel an interview, setting the status
and saving the form data (likely a reason for cancellation). and saving the form data (likely a reason for cancellation).
""" """
scheduled_interview = get_object_or_404(ScheduledInterview) scheduled_interview = get_object_or_404(ScheduledInterview,slug=slug)
form = InterviewCancelForm(request.POST, instance=scheduled_interview) form = InterviewCancelForm(request.POST, instance=scheduled_interview)
if form.is_valid(): if form.is_valid():
@ -4275,6 +4281,33 @@ def cancel_interview_for_application(request, slug):
return redirect("interview_detail", slug=scheduled_interview.slug) return redirect("interview_detail", slug=scheduled_interview.slug)
@require_POST
@login_required # Assuming this should be protected
@staff_user_required # Assuming only staff can cancel
def update_interview_result(request,slug):
interview = get_object_or_404(Interview,slug=slug)
schedule=interview.scheduled_interview
form = InterviewResultForm(request.POST, instance=interview)
if form.is_valid():
interview.save(update_fields=['interview_result', 'result_comments'])
form.save() # Saves form data
messages.success(request, _("Interview cancelled successfully."))
return redirect("interview_detail", slug=schedule.slug)
else:
error_list = [
f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()
]
error_message = _("Please correct the following errors: ") + " ".join(
error_list
)
messages.error(request, error_message)
return redirect("interview_detail", slug=schedule.slug)
@login_required @login_required
@staff_user_required @staff_user_required
def agency_access_link_deactivate(request, slug): def agency_access_link_deactivate(request, slug):
@ -4389,161 +4422,85 @@ def api_application_detail(request, candidate_id):
return JsonResponse({"success": False, "error": str(e)}) return JsonResponse({"success": False, "error": str(e)})
@login_required # @login_required
@staff_user_required # @staff_user_required
def compose_application_email(request, slug): # def compose_application_email(request, slug):
"""Compose email to participants about a candidate""" # """Compose email to participants about a candidate"""
from .email_service import send_bulk_email # from django.conf import settings
from .services.email_service import EmailService
from .dto.email_dto import BulkEmailConfig, EmailPriority
job = get_object_or_404(JobPosting, slug=slug) # job = get_object_or_404(JobPosting, slug=slug)
candidate_ids = request.GET.getlist("candidate_ids") # candidate_ids = request.GET.getlist("candidate_ids")
candidates = Application.objects.filter(id__in=candidate_ids) # candidates = Application.objects.filter(id__in=candidate_ids)
if request.method == "POST": # if request.method == "POST":
candidate_ids = request.POST.getlist("candidate_ids") # candidate_ids = request.POST.getlist("candidate_ids")
applications = Application.objects.filter(id__in=candidate_ids) # applications = Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, applications, request.POST) # form = CandidateEmailForm(job, applications, request.POST)
if form.is_valid(): # if form.is_valid():
# Get email addresses # # Get email addresses
email_addresses = form.get_email_addresses() # email_addresses = form.get_email_addresses()
if not email_addresses: # if not email_addresses:
messages.error(request, "No email selected") # messages.error(request, "No email selected")
referer = request.META.get("HTTP_REFERER") # referer = request.META.get("HTTP_REFERER")
if referer: # if referer:
# Redirect back to the referring page # # Redirect back to the referring page
return redirect(referer) # return redirect(referer)
else: # else:
return redirect("dashboard") # return redirect("dashboard")
subject = form.cleaned_data.get("subject") # subject = form.cleaned_data.get("subject")
message = form.get_formatted_message() # message = form.get_formatted_message()
service = EmailService()
# async_task(
# "recruitment.tasks.send_bulk_email_task",
# email_addresses,
# subject,
# # message,
# "emails/email_template.html",
# {
# "job": job,
# "applications": applications,
# "email_message": message,
# "logo_url": settings.STATIC_URL + "image/kaauh.png",
# },
# )
# return redirect(request.path)
# Prepare recipients data for bulk email # else:
# recipients_data = [] # # Form validation errors
# for email_addr in email_addresses: # messages.error(request, "Please correct the errors below.")
# recipients_data.append(
# {
# "email": email_addr,
# "name": email_addr.split("@")[0]
# if "@" in email_addr
# else email_addr,
# }
# )
from django.conf import settings
async_task(
"recruitment.tasks.send_bulk_email_task",
email_addresses,
subject,
# message,
"emails/email_template.html",
{
"job": job,
"applications": applications,
"email_message": message,
"logo_url": settings.STATIC_URL + "image/kaauh.png",
},
)
# Create bulk email configuration
# bulk_config = BulkEmailConfig(
# subject=subject,
# recipients_data=recipients_data,
# attachments=None,
# sender=request.user if request and hasattr(request, "user") else None,
# job=job,
# priority=EmailPriority.NORMAL,
# async_send=True,
# )
# Send bulk emails # # For HTMX requests, return error response
# if email_result["success"]: # if "HX-Request" in request.headers:
# for application in applications: # return JsonResponse(
# if hasattr(application, "person") and application.person: # {
# try: # "success": False,
# Message.objects.create( # "error": "Please correct the form errors and try again.",
# sender=request.user, # }
# recipient=application.person.user, # )
# subject=subject,
# content=message,
# job=job,
# message_type="job_related",
# is_email_sent=True,
# email_address=application.person.email
# if application.person.email
# else application.email,
# )
# except Exception as e: # return render(
# # Log error but don't fail the entire process # request,
# print(f"Error creating message") # "includes/email_compose_form.html",
# {"form": form, "job": job, "candidates": candidates},
# )
# messages.success( # else:
# request, # # GET request - show the form
# f"Email will be sent shortly to recipient(s)", # form = CandidateEmailForm(job, candidates)
# )
# response = HttpResponse(status=200)
# response.headers["HX-Refresh"] = "true"
# return response
# # return redirect("applications_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 # return render(
# if "HX-Request" in request.headers: # request,
# return JsonResponse( # "includes/email_compose_form.html",
# { # # {"form": form, "job": job, "candidates": candidates},
# "success": False, # {"form": form, "job": job},
# "error": email_result.get( # )
# "message", "Failed to send email"
# ),
# }
# )
# return render(
# request,
# "includes/email_compose_form.html",
# {"form": form, "job": job, "candidate": candidates},
# )
else:
# Form validation 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, "candidates": candidates},
)
else:
# GET request - show the form
form = CandidateEmailForm(job, candidates)
return render(
request,
"includes/email_compose_form.html",
# {"form": form, "job": job, "candidates": candidates},
{"form": form, "job": job},
)
# Source CRUD Views # Source CRUD Views
@ -4799,6 +4756,7 @@ def application_signup(request, slug):
@login_required @login_required
@staff_user_required @staff_user_required
def interview_list(request): def interview_list(request):
"""List all interviews with filtering and pagination""" """List all interviews with filtering and pagination"""
interviews = ScheduledInterview.objects.select_related( interviews = ScheduledInterview.objects.select_related(
"application", "application",
@ -4825,7 +4783,7 @@ def interview_list(request):
interviews = interviews.filter( interviews = interviews.filter(
Q(application__person__first_name=search_query) Q(application__person__first_name=search_query)
| Q(application__person__last_name__icontains=search_query) | Q(application__person__last_name__icontains=search_query)
| Q(application__person__email=search_query) | Q(application__person__email__icontains=search_query)
| Q(job__title__icontains=search_query) | Q(job__title__icontains=search_query)
) )
@ -4854,8 +4812,11 @@ def interview_detail(request, slug):
OnsiteScheduleInterviewUpdateForm, OnsiteScheduleInterviewUpdateForm,
) )
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview interview = schedule.interview
interview_result_form=InterviewResultForm(instance=interview)
application = schedule.application application = schedule.application
job = schedule.job job = schedule.job
print(interview.location_type) print(interview.location_type)
@ -4878,6 +4839,7 @@ def interview_detail(request, slug):
"interview_status_form": ScheduledInterviewUpdateStatusForm(), "interview_status_form": ScheduledInterviewUpdateStatusForm(),
"cancel_form": InterviewCancelForm(instance=meeting), "cancel_form": InterviewCancelForm(instance=meeting),
"interview_email_form": interview_email_form, "interview_email_form": interview_email_form,
"interview_result_form":interview_result_form,
} }
return render(request, "interviews/interview_detail.html", context) return render(request, "interviews/interview_detail.html", context)
@ -6487,55 +6449,196 @@ def sync_history(request, job_slug=None):
return render(request, "recruitment/sync_history.html", context) return render(request, "recruitment/sync_history.html", context)
# def send_interview_email(request, slug):
# from django.conf import settings
# schedule = get_object_or_404(ScheduledInterview, slug=slug)
# application = schedule.application
# job = application.job
# form = InterviewEmailForm(job, application, schedule)
# if request.method == "POST":
# form = InterviewEmailForm(job, application, schedule, request.POST)
# if form.is_valid():
# recipient = form.cleaned_data.get("to").strip()
# body_message = form.cleaned_data.get("message")
# subject = form.cleaned_data.get("subject")
# sender_user = request.user
# job = job
# try:
# # Send email using background task
# email_result= async_task(
# "recruitment.tasks.send_bulk_email_task",
# recipient,
# subject,
# # message,
# "emails/email_template.html",
# {
# "job": job,
# "applications": application,
# "email_message":body_message,
# "logo_url": settings.STATIC_URL + "image/kaauh.png",
# },
# )
# if email_result:
# messages.success(request, "Message sent successfully via email!")
# else:
# messages.warning(
# request,
# f"email failed: {email_result.get('message', 'Unknown error')}",
# )
# except Exception as e:
# messages.warning(
# request, f"Message saved but email sending failed: {str(e)}"
# )
# else:
# form = InterviewEmailForm(job, application, schedule)
# else: # GET request
# form = InterviewEmailForm(job, application, schedule)
# # This is the final return, which handles GET requests and invalid POST requests.
# return redirect("interview_detail", slug=schedule.slug)
def send_interview_email(request, slug): def send_interview_email(request, slug):
from django.conf import settings
from django_q.tasks import async_task
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
application = schedule.application application = schedule.application
job = application.job job = application.job
form = InterviewEmailForm(job, application, schedule)
if request.method == "POST": if request.method == "POST":
form = InterviewEmailForm(job, application, schedule, request.POST) form = InterviewEmailForm(job, application, schedule, request.POST)
if form.is_valid(): if form.is_valid():
recipient = form.cleaned_data.get("to").strip() # 1. Ensure recipient is a list (fixes the "@" error)
recipient_str = form.cleaned_data.get("to").strip()
recipient_list = [recipient_str]
body_message = form.cleaned_data.get("message") body_message = form.cleaned_data.get("message")
subject = form.cleaned_data.get("subject") subject = form.cleaned_data.get("subject")
sender = request.user
job = job
try: try:
# Use new unified email service for background processing # 2. Match the context expected by your task/service
from .services.email_service import UnifiedEmailService # We pass IDs for the sender/job to avoid serialization issues
from .dto.email_dto import EmailConfig, EmailPriority async_task(
"recruitment.tasks.send_email_task",
service = UnifiedEmailService() recipient_list,
subject,
# Create email configuration "emails/email_template.html",
config = EmailConfig( {
to_email=recipient, "job": job, # Useful for Message creation
subject=subject, "sender_user": request.user,
html_content=body_message, "applications": application,
attachments=None, "email_message": body_message,
sender=sender, "message_created":False,
job=job, "logo_url": settings.STATIC_URL + "image/kaauh.png",
priority=EmailPriority.NORMAL, },
) )
# Send email using background task messages.success(request, "Interview email enqueued successfully!")
email_result = service.send_email(config) return redirect("interview_detail", slug=schedule.slug)
if email_result:
messages.success(request, "Message sent successfully via email!")
else:
messages.warning(
request,
f"email failed: {email_result.get('message', 'Unknown error')}",
)
except Exception as e: except Exception as e:
messages.warning( messages.error(request, f"Task scheduling failed: {str(e)}")
request, f"Message saved but email sending failed: {str(e)}"
)
else: else:
form = InterviewEmailForm(job, application, schedule) messages.error(request, "Please correct the errors in the form.")
else: # GET request else:
# GET request
form = InterviewEmailForm(job, application, schedule) form = InterviewEmailForm(job, application, schedule)
# This is the final return, which handles GET requests and invalid POST requests. # 3. FIX: Instead of always redirecting, render the template
return redirect("interview_detail", slug=schedule.slug) # This allows users to see validation errors.
return render(
request,
"recruitment/interview_email_form.html", # Replace with your actual template path
{
"form": form,
"schedule": schedule,
"job": job
}
)
@login_required
@staff_user_required
def compose_application_email(request, slug):
"""Compose email to participants about a candidate"""
from django.conf import settings
job = get_object_or_404(JobPosting, slug=slug)
candidate_ids = request.GET.getlist("candidate_ids")
candidates = Application.objects.filter(id__in=candidate_ids)
if request.method == "POST":
candidate_ids = request.POST.getlist("candidate_ids")
applications = Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, applications, request.POST)
if form.is_valid():
# Get email addresses
email_addresses = form.get_email_addresses()
if not email_addresses:
messages.error(request, "No email selected")
referer = request.META.get("HTTP_REFERER")
if referer:
# Redirect back to the referring page
return redirect(referer)
else:
return redirect("dashboard")
subject = form.cleaned_data.get("subject")
message = form.get_formatted_message()
async_task(
"recruitment.tasks.send_email_task",
email_addresses,
subject,
# message,
"emails/email_template.html",
{
"job": job,
"sender_user": request.user,
"applications": applications,
"email_message": message,
"message_created":False,
"logo_url": settings.STATIC_URL + "image/kaauh.png",
},
)
return redirect(request.path)
else:
# Form validation 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, "candidates": candidates},
)
else:
# GET request - show the form
form = CandidateEmailForm(job, candidates)
return render(
request,
"includes/email_compose_form.html",
# {"form": form, "job": job, "candidates": candidates},
{"form": form, "job": job},
)

View File

@ -360,6 +360,7 @@
</form> </form>
{% endif %} {% endif %}
</li> </li>
{% if request.user.is_authenticated%}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button class="language-toggle-btn dropdown-toggle" type="button" <button class="language-toggle-btn dropdown-toggle" type="button"
data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false" data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false"
@ -410,11 +411,13 @@
<i class="fas fa-tachometer-alt me-3 fs-5"></i> <span>{% trans "Dashboard" %}</span> <i class="fas fa-tachometer-alt me-3 fs-5"></i> <span>{% trans "Dashboard" %}</span>
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-primary-theme" href="{% url 'user_detail' request.user.pk %}"> <a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-primary-theme" href="{% url 'user_detail' request.user.pk %}" >
<i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span> <i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span>
</a> </a>
</li> </li>
<li><hr class="dropdown-divider my-1"></li> <li><hr class="dropdown-divider my-1"></li>
<li> <li>
@ -432,6 +435,8 @@
</li> </li>
</ul> </ul>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -670,22 +670,9 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="post" action="#"> <form method="post" action="{% url 'update_interview_result' interview.slug %}">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> {{interview_result_form|crispy}}
<label for="interview_result" class="form-label">{% trans "Interview Result" %}</label>
<select class="form-select" id="interview_result" name="result" required>
<option value="">{% trans "Select Result" %}</option>
<option value="passed">{% trans "Passed" %}</option>
<option value="failed">{% trans "Failed" %}</option>
<option value="on_hold">{% trans "On Hold" %}</option>
</select>
</div>
<div class="mb-3">
<label for="result_notes" class="form-label">{% trans "Notes" %}</label>
<textarea class="form-control" id="result_notes" name="notes" rows="4"
placeholder="{% trans 'Add interview feedback and notes' %}"></textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm mt-2"> <button type="submit" class="btn btn-main-action btn-sm mt-2">
<i class="fas fa-check me-1"></i> {% trans "Update Result" %} <i class="fas fa-check me-1"></i> {% trans "Update Result" %}
</button> </button>

View File

@ -502,7 +502,7 @@
- -
{% endif %} {% endif %}
</td> </td>
<td> <td id="document-{{ document.id }}">
{% if document.file %} {% if document.file %}
<a href="{{ document.file.url }}" <a href="{{ document.file.url }}"
class="btn btn-sm btn-outline-primary me-1" class="btn btn-sm btn-outline-primary me-1"
@ -510,9 +510,15 @@
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
{% trans "Download" %} {% trans "Download" %}
</a> </a>
<button class="btn btn-sm btn-outline-danger" onclick="deleteDocument({{ document.id }})">
<i class="fas fa-trash"></i>
</button> <button hx-post="{% url 'document_delete' document.pk %}"
hx-target="#document-{{ document.id }}"
hx-swap="outerHTML"
hx-confirm="{% trans 'Are you sure you want to delete this file?' %}"
class="btn btn-sm btn-danger">
<i class="fas fa-trash"></i>
</button>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@ -657,7 +663,7 @@ function addToCalendar(year, month, day, time, title) {
window.open(googleCalendarUrl, '_blank'); window.open(googleCalendarUrl, '_blank');
} }
{% comment %}
function deleteDocument(documentId) { function deleteDocument(documentId) {
if (confirm('{% trans "Are you sure you want to delete this document?" %}')) { if (confirm('{% trans "Are you sure you want to delete this document?" %}')) {
fetch(`/documents/${documentId}/delete/`, { fetch(`/documents/${documentId}/delete/`, {
@ -696,7 +702,7 @@ function getCookie(name) {
} }
} }
return cookieValue; return cookieValue;
} } {% endcomment %}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -572,7 +572,7 @@
<a href="{{ document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a> <a href="{{ document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a>
{# HTMX DELETE BUTTON #} {# HTMX DELETE BUTTON #}
<button hx-post="{% url 'application_document_delete' document.id %}" <button hx-post="{% url 'document_delete' document.pk %}"
hx-target="#document-{{ document.id }}" hx-target="#document-{{ document.id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-confirm="{% trans 'Are you sure you want to delete this file?' %}" hx-confirm="{% trans 'Are you sure you want to delete this file?' %}"

View File

@ -92,14 +92,14 @@
class="btn btn-sm btn-outline-secondary" title="Edit Setting"> class="btn btn-sm btn-outline-secondary" title="Edit Setting">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<form method="post" action="{% url 'settings_delete' pk=setting.pk %}" {% comment %} <form method="post" action="{% url 'settings_delete' pk=setting.pk %}"
onsubmit="return confirm('Are you sure you want to delete this setting?');" onsubmit="return confirm('Are you sure you want to delete this setting?');"
style="display: inline;"> style="display: inline;">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete Setting"> <button type="submit" class="btn btn-sm btn-outline-danger" title="Delete Setting">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</form> </form> {% endcomment %}
</div> </div>
</td> </td>
</tr> </tr>