working on translation

This commit is contained in:
ismail 2025-11-20 18:55:11 +03:00
parent 93eac071ab
commit e1a55d7633
14 changed files with 959 additions and 20205 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1310,7 +1310,18 @@ class ScheduledInterview(Base):
models.Index(fields=["application", "job"]), models.Index(fields=["application", "job"]),
] ]
@property
def get_schedule_type(self):
if self.schedule:
return self.schedule.schedule_interview_type
else:
return self.interview_location.location_type
@property
def get_schedule_status(self):
return self.status
@property
def get_meeting_details(self):
return self.interview_location
# --- 3. Interview Notes Model (Fixed) --- # --- 3. Interview Notes Model (Fixed) ---
class InterviewNote(Base): class InterviewNote(Base):

View File

@ -442,6 +442,11 @@ urlpatterns = [
views.candidate_application_detail, views.candidate_application_detail,
name="candidate_application_detail", name="candidate_application_detail",
), ),
# path(
# "candidate/<slug:application_slug>/applications/<slug:person_slug>/detail/<slug:agency_slug>/",
# views.candidate_application_detail,
# name="candidate_application_detail",
# ),
path( path(
"portal/dashboard/", "portal/dashboard/",
views.agency_portal_dashboard, views.agency_portal_dashboard,
@ -661,4 +666,8 @@ urlpatterns = [
# Detail View (assuming slug is on ScheduledInterview) # Detail View (assuming slug is on ScheduledInterview)
path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"), path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
# Email invitation URLs
path("interviews/meetings/<slug:slug>/send-candidate-invitation/", views.send_candidate_invitation, name="send_candidate_invitation"),
path("interviews/meetings/<slug:slug>/send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"),
] ]

View File

@ -129,7 +129,6 @@ from .models import (
Source, Source,
Message, Message,
Document, Document,
OnsiteLocationDetails,
InterviewLocation, InterviewLocation,
InterviewNote InterviewNote
) )
@ -1964,8 +1963,6 @@ def candidate_update_status(request, slug):
@staff_user_required @staff_user_required
def candidate_interview_view(request, slug): def candidate_interview_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
context = { context = {
"job": job, "job": job,
"candidates": job.interview_candidates, "candidates": job.interview_candidates,
@ -1975,8 +1972,6 @@ def candidate_interview_view(request, slug):
return render(request, "recruitment/candidate_interview_view.html", context) return render(request, "recruitment/candidate_interview_view.html", context)
@staff_user_required @staff_user_required
def candidate_document_review_view(request, slug): def candidate_document_review_view(request, slug):
""" """
@ -4096,6 +4091,11 @@ def candidate_application_detail(request, slug):
return redirect("account_login") return redirect("account_login")
# Get candidate profile (Person record) # Get candidate profile (Person record)
agency = getattr(request.user,"agency_profile",None)
if agency:
candidate = get_object_or_404(Application,slug=slug)
# if Application.objects.filter(person=candidate,hirin).exists()
else:
try: try:
candidate = request.user.person_profile candidate = request.user.person_profile
except: except:
@ -4110,7 +4110,7 @@ def candidate_application_detail(request, slug):
'scheduled_interviews' # Only prefetch interviews, not documents (Generic FK) 'scheduled_interviews' # Only prefetch interviews, not documents (Generic FK)
), ),
slug=slug, slug=slug,
person=candidate person=candidate.person if agency else candidate
) )
# Get AI analysis data if available # Get AI analysis data if available
@ -4989,7 +4989,7 @@ def document_upload(request, slug):
if upload_target == 'person': if upload_target == 'person':
return redirect("candidate_portal_dashboard") return redirect("candidate_portal_dashboard")
else: else:
return redirect("candidate_application_detail", slug=application.slug) return redirect("candidate_application_detail", application_slug=application.slug)
# Handle GET request for AJAX # Handle GET request for AJAX
if request.headers.get("X-Requested-With") == "XMLHttpRequest": if request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -6108,3 +6108,152 @@ def meeting_details(request, slug):
return render(request, 'interviews/detail_interview.html', context) return render(request, 'interviews/detail_interview.html', context)
@login_required
def send_candidate_invitation(request, slug):
"""Send invitation email to the candidate"""
meeting = get_object_or_404(InterviewLocation, slug=slug)
try:
interview = meeting.scheduled_interview
except ScheduledInterview.DoesNotExist:
raise Http404("No interview is associated with this meeting.")
candidate = interview.application
job = interview.job
if request.method == 'POST':
try:
from django.core.mail import send_mail
from django.conf import settings
# Simple email content
subject = f"Interview Invitation - {job.title}"
message = f"""
Dear {candidate.person.first_name} {candidate.person.last_name},
You are invited for an interview for the position of {job.title}.
Meeting Details:
- Date: {interview.interview_date}
- Time: {interview.interview_time}
- Duration: {meeting.duration or 60} minutes
"""
# Add join URL if it's a Zoom meeting
if hasattr(meeting, 'zoommeetingdetails') and meeting.zoommeetingdetails.join_url:
message += f"- Join URL: {meeting.zoommeetingdetails.join_url}\n"
# Add physical address if it's an onsite meeting
if hasattr(meeting, 'onsitelocationdetails') and meeting.onsitelocationdetails.physical_address:
message += f"- Location: {meeting.onsitelocationdetails.physical_address}\n"
if meeting.onsitelocationdetails.room_number:
message += f"- Room: {meeting.onsitelocationdetails.room_number}\n"
message += """
Please confirm your attendance.
Best regards,
KAAUH Recruitment Team
"""
# Send email
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[candidate.person.email],
fail_silently=False,
)
messages.success(request, f"Invitation email sent to {candidate.person.email}")
except Exception as e:
messages.error(request, f"Failed to send invitation email: {str(e)}")
return redirect('meeting_details', slug=slug)
@login_required
def send_participants_invitation(request, slug):
"""Send invitation email to all participants"""
meeting = get_object_or_404(InterviewLocation, slug=slug)
try:
interview = meeting.scheduled_interview
except ScheduledInterview.DoesNotExist:
raise Http404("No interview is associated with this meeting.")
candidate = interview.application
job = interview.job
if request.method == 'POST':
try:
from django.core.mail import send_mail
from django.conf import settings
# Get all participants
participants = list(interview.participants.all())
system_users = list(interview.system_users.all())
all_participants = participants + system_users
if not all_participants:
messages.warning(request, "No participants found to send invitation to.")
return redirect('meeting_details', slug=slug)
# Simple email content
subject = f"Interview Invitation - {job.title} with {candidate.person.first_name} {candidate.person.last_name}"
message = f"""
Dear Team Member,
You are invited to participate in an interview session.
Interview Details:
- Candidate: {candidate.person.first_name} {candidate.person.last_name}
- Position: {job.title}
- Date: {interview.interview_date}
- Time: {interview.interview_time}
- Duration: {meeting.duration or 60} minutes
"""
# Add join URL if it's a Zoom meeting
if hasattr(meeting, 'zoommeetingdetails') and meeting.zoommeetingdetails.join_url:
message += f"- Join URL: {meeting.zoommeetingdetails.join_url}\n"
# Add physical address if it's an onsite meeting
if hasattr(meeting, 'onsitelocationdetails') and meeting.onsitelocationdetails.physical_address:
message += f"- Location: {meeting.onsitelocationdetails.physical_address}\n"
if meeting.onsitelocationdetails.room_number:
message += f"- Room: {meeting.onsitelocationdetails.room_number}\n"
message += """
Please confirm your availability.
Best regards,
KAAUH Recruitment Team
"""
# Get email addresses of all participants
recipient_emails = []
for participant in all_participants:
if hasattr(participant, 'email') and participant.email:
recipient_emails.append(participant.email)
if recipient_emails:
# Send email to all participants
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
recipient_emails,
fail_silently=False,
)
messages.success(request, f"Invitation emails sent to {len(recipient_emails)} participants")
else:
messages.warning(request, "No valid email addresses found for participants.")
except Exception as e:
messages.error(request, f"Failed to send invitation emails: {str(e)}")
return redirect('meeting_details', slug=slug)

View File

@ -171,12 +171,12 @@
title="{% trans 'Your account' %}"> title="{% trans 'Your account' %}">
{% else %} {% else %}
<div class="profile-avatar" title="{% trans 'Your account' %}"> <div class="profile-avatar" title="{% trans 'Your account' %}">
{{ user.username|first|upper }} {{ user.first_name }} {{ user.last_name }}
</div> </div>
{% endif %} {% endif %}
</button> </button>
<ul
<ul
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3" class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
style="min-width: 240px;" style="min-width: 240px;"
> >
@ -242,10 +242,6 @@
<span style="color:red;">{% trans "Sign Out" %}</span> <span style="color:red;">{% trans "Sign Out" %}</span>
</button> </button>
</form> </form>
{% comment %} <a class="d-inline text-decoration-none px-4 d-flex align-items-center border-0 bg-transparent text-start text-center" href={% url "account_logout" %}>
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
<span style="color:red;">{% trans "Sign Out" %}</span>
</a> {% endcomment %}
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@ -226,7 +226,7 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
</div> </div>
{# --- PARTICIPANTS --- #} {# --- PARTICIPANTS --- #}
<div class="row g-4 mt-1 mb-5"> {% comment %} <div class="row g-4 mt-1 mb-5">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm"> <div class="p-3 bg-white rounded shadow-sm">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
@ -275,7 +275,7 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
</table> </table>
</div> </div>
</div> </div>
</div> </div> {% endcomment %}
{# --- COMMENTS --- #} {# --- COMMENTS --- #}
<div class="row g-4 mt-1"> <div class="row g-4 mt-1">

View File

@ -216,6 +216,15 @@ body {
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary-teal btn-sm"> <a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary-teal btn-sm">
<i class="fas fa-edit me-1"></i> {% trans "Edit Meeting" %} <i class="fas fa-edit me-1"></i> {% trans "Edit Meeting" %}
</a> </a>
{# Send Candidate Invitation Button #}
<form method="post" action="{% url 'send_candidate_invitation' meeting.slug %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="btn btn-outline-info btn-sm" onclick="return confirm('{% trans "Send invitation email to the candidate?" %}')">
<i class="fas fa-envelope me-1"></i> {% trans "Send Candidate Invitation" %}
</button>
</form>
{# DELETE MEETING FORM #} {# DELETE MEETING FORM #}
<form method="post" action="{% url 'delete_meeting' meeting.slug %}" style="display: inline;"> <form method="post" action="{% url 'delete_meeting' meeting.slug %}" style="display: inline;">
{% csrf_token %} {% csrf_token %}
@ -298,7 +307,7 @@ body {
{# --- PARTICIPANTS TABLE --- #} {# --- PARTICIPANTS TABLE --- #}
<div class="col-lg-12"> {% comment %} <div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm"> <div class="p-3 bg-white rounded shadow-sm">
<div class="d-flex justify-content-between align-item-center" > <div class="d-flex justify-content-between align-item-center" >
<h2 class="text-start"><i class="fas fa-users-cog me-2"></i> {% trans "Assigned Participants" %}</h2> <h2 class="text-start"><i class="fas fa-users-cog me-2"></i> {% trans "Assigned Participants" %}</h2>
@ -310,6 +319,14 @@ body {
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{total_participants}}) <i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{total_participants}})
</button> </button>
{# Send Participants Invitation Button #}
<form method="post" action="{% url 'send_participants_invitation' meeting.slug %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="btn btn-outline-info btn-sm me-2" onclick="return confirm('{% trans "Send invitation email to all participants?" %}')">
<i class="fas fa-envelope me-1"></i> {% trans "Send Participants Invitation" %}
</button>
</form>
<button type="button" class="btn btn-outline-info" <button type="button" class="btn btn-outline-info"
data-bs-toggle="modal" data-bs-toggle="modal"
title="Send Interview Emails" title="Send Interview Emails"
@ -352,7 +369,7 @@ body {
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div> {% endcomment %}
</div> </div>
{# ========================================================= #} {# ========================================================= #}

View File

@ -1,7 +1,7 @@
{% extends "portal_base.html" %} {% extends "portal_base.html" %}
{% load static %} {% load static %}
{% block title %}{% if form.instance.pk %}Reply to Message{% else %}Compose Message{% endif %}{% endblock %} {% block title %}{% if form.instance.pk %}{% trans "Reply to Message"%}{% else %}{% trans"Compose Message"%}{% endif %}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">

View File

@ -194,7 +194,7 @@ document.addEventListener('DOMContentLoaded', function() {
function updateCounter() { function updateCounter() {
const remaining = maxLength - subjectField.value.length; const remaining = maxLength - subjectField.value.length;
counter.textContent = `${subjectField.value.length}/${maxLength} characters`; counter.textContent = `{% blocktrans %}{{ remaining }}/{{ maxLength }} characters{% endblocktrans %}`;
if (remaining < 20) { if (remaining < 20) {
counter.className = 'text-warning'; counter.className = 'text-warning';
} else { } else {
@ -236,3 +236,4 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -228,3 +228,4 @@ setInterval(() => {
}, 30000); }, 30000);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -264,6 +264,9 @@
</div> </div>
</td> </td>
<td> <td>
<a href="{% url 'candidate_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
<i class="fas fa-eye"></i>
</a>
<button class="btn btn-sm btn-outline-primary" onclick="editCandidate({{ candidate.id }})" title="{% trans 'Edit Candidate' %}"> <button class="btn btn-sm btn-outline-primary" onclick="editCandidate({{ candidate.id }})" title="{% trans 'Edit Candidate' %}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>

View File

@ -178,7 +178,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid">
<!-- Breadcrumb Navigation -->
{% if not application.hiring_agency %}
<nav aria-label="breadcrumb" class="mb-4"> <nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
@ -195,6 +197,7 @@
</li> </li>
</ol> </ol>
</nav> </nav>
{% endif %}
<div class="row mb-4"> <div class="row mb-4">
@ -355,7 +358,7 @@
<div class="row mb-5"> <div class="row mb-5">
<div class="col-12"> <div class="col-12">
<div class="kaauh-card"> <div class="kaauh-card">
<div class="card-header bg-success text-white"> <div class="card-header bg-primary-theme text-white">
<h5 class="mb-0"> <h5 class="mb-0">
<i class="fas fa-video me-2"></i> <i class="fas fa-video me-2"></i>
{% trans "Interview Schedule" %} {% trans "Interview Schedule" %}
@ -381,28 +384,28 @@
<td>{{ interview.interview_date|date:"M d, Y" }}</td> <td>{{ interview.interview_date|date:"M d, Y" }}</td>
<td>{{ interview.interview_time|time:"H:i" }}</td> <td>{{ interview.interview_time|time:"H:i" }}</td>
<td> <td>
{% if interview.zoom_meeting %} {% if interview.get_schedule_type == 'Remote' %}
<span class="badge bg-primary-theme"> <span class="badge bg-primary-theme">
<i class="fas fa-laptop me-1"></i> <i class="fas fa-laptop me-1"></i>
{% trans "Remote" %} {% trans "Remote" %}
</span> </span>
{% else %} {% else %}
<span class="badge bg-secondary"> <span class="badge bg-primary-theme">
<i class="fas fa-building me-1"></i> <i class="fas fa-building me-1"></i>
{% trans "On-site" %} {% trans "On-site" %}
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<span class="badge bg-{{ interview.status|lower }} text-white"> <span class="badg ">
{{ interview.get_status_display }} {{ interview.get_schedule_status }}
</span> </span>
</td> </td>
<td> <td>
{% if interview.zoom_meeting and interview.zoom_meeting.join_url %} {% if interview.get_meeting_details and interview.get_schedule_type == 'Remote' %}
<a href="{{ interview.zoom_meeting.join_url }}" <a href="{{ interview.get_meeting_details }}"
target="_blank" target="_blank"
class="btn btn-sm btn-primary"> class="btn btn-sm bg-primary-theme text-white">
<i class="fas fa-video me-1"></i> <i class="fas fa-video me-1"></i>
{% trans "Join" %} {% trans "Join" %}
</a> </a>
@ -533,6 +536,62 @@
{% endif %} {% endif %}
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="kaauh-card h-100">
<div class="card-body text-center">
<i class="fas fa-arrow-left fa-2x text-primary-theme mb-3"></i>
<h6>{% trans "Back to Dashboard" %}</h6>
<p class="text-muted small">{% trans "View all your applications" %}</p>
{% if application.hiring_agency %}
<a href="{% url 'agency_portal_dashboard' %}" class="btn btn-main-action w-100">
{% trans "Go Back" %}
</a>
{% else %}
<a href="{% url 'candidate_portal_dashboard' %}" class="btn btn-main-action w-100">
{% trans "Go to Dashboard" %}
</a>
{% endif %}
</div>
</div>
</div>
{% if application.resume %}
<div class="col-md-3 mb-3">
<div class="kaauh-card h-100">
<div class="card-body text-center">
<i class="fas fa-download fa-2x text-primary-theme mb-3"></i>
<h6>{% trans "Download Resume" %}</h6>
<p class="text-muted small">{% trans "Get your submitted resume" %}</p>
<a href="{{ application.resume.url }}"
target="_blank"
class="btn btn-main-action w-100">
<i class="fas fa-download me-2"></i>
{% trans "Download" %}
</a>
</div>
</div>
</div>
{% endif %}
{% comment %} <div class="col-md-3 mb-3">
<div class="kaauh-card h-100">
<div class="card-body text-center">
<i class="fas fa-edit fa-2x text-warning mb-3"></i>
<h6>{% trans "Update Profile" %}</h6>
<p class="text-muted small">{% trans "Edit your personal information" %}</p>
<a href="" class="btn btn-main-action w-100">
<i class="fas fa-edit me-2"></i>
{% trans "Update" %}
</a>
</div>
</div>
</div> {% endcomment %}
</div>
<!-- Next Steps Section -->
{% comment %} <div class="row">
<div class="col-12"> <div class="col-12">
<div class="kaauh-card"> <div class="kaauh-card">
<div class="card-header bg-primary-theme text-white"> <div class="card-header bg-primary-theme text-white">