Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend
This commit is contained in:
commit
3eebb51f0b
6
.env
6
.env
@ -1,3 +1,3 @@
|
|||||||
DB_NAME=haikal_db
|
DB_NAME=norahuniversity
|
||||||
DB_USER=faheed
|
DB_USER=norahuniversity
|
||||||
DB_PASSWORD=Faheed@215
|
DB_PASSWORD=norahuniversity
|
||||||
188
base.po
Normal file
188
base.po
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Big SaaS App 2.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-05-20 10:00+0000\n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: es\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "My Profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Account Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Billing & Invoices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log Out"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Email Address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Remember me on this device"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Forgot your password?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Don't have an account? Sign up."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Ensure this field has at least %(limit_value)d characters (it has %(show_value)d)."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "noun"
|
||||||
|
msgid "Book"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "verb"
|
||||||
|
msgid "Book"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "month_name"
|
||||||
|
msgid "May"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "auxiliary_verb"
|
||||||
|
msgid "May"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Product Description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add to Cart"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Proceed to Checkout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Total: $%(amount).2f"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Shipping Address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Order History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Delete Account"
|
||||||
|
msgstr "Borrar cuenta permanentemente ahora mismo"
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Save Changes"
|
||||||
|
msgstr "Guardar cosas"
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Upload Avatar"
|
||||||
|
msgstr "Subir foto"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Welcome to the platform. By using our services, you agree to our <a "
|
||||||
|
"href='%(terms_url)s'>Terms of Service</a> and <a "
|
||||||
|
"href='%(privacy_url)s'>Privacy Policy</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Please check your email inbox. We have sent a confirmation link to verify "
|
||||||
|
"your account ownership. The link will expire in 24 hours."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "<strong>Warning:</strong> This action cannot be undone."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid "404 - Page Not Found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Internal Server Error (500)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "API Connection Timeout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid CSRF Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Monday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Tuesday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Wednesday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Thursday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Friday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Saturday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sunday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Just now"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "%(count)s minutes ago"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Step 1 of 5"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Skip tutorial"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Finish"
|
||||||
|
msgstr ""
|
||||||
37
demo.po
Normal file
37
demo.po
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Test Project 1.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-03-15 12:00+0000\n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: fr\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
|
msgid "Hello, world!"
|
||||||
|
msgstr "مرحبا، العالم!"
|
||||||
|
|
||||||
|
msgid "Welcome back, %s."
|
||||||
|
msgstr "مرحبًا بعودتك، %s."
|
||||||
|
|
||||||
|
msgid "User %(username)s has logged in."
|
||||||
|
msgstr "مستخدم %(username)s تم تسجيله دخولًا."
|
||||||
|
|
||||||
|
msgid "Please click <a href='%(url)s'>here</a> to reset your password."
|
||||||
|
msgstr ""
|
||||||
|
"رجاءً انقر على <a href='%(url)s'>هنا</a> للرجوع لكلمة المرور الخاصة بك."
|
||||||
|
|
||||||
|
msgid "Database connection failed: PostgreSQL error."
|
||||||
|
msgstr "فشل اتصال البيانات: خطأ PostgreSQL."
|
||||||
|
|
||||||
|
msgid "Good morning"
|
||||||
|
msgstr "صباح الخير"
|
||||||
|
|
||||||
|
msgctxt "button_label"
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "حفظ"
|
||||||
167
demo1.po
Normal file
167
demo1.po
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Big SaaS App 2.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-05-20 10:00+0000\n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: es\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "شاشة رئيسية"
|
||||||
|
|
||||||
|
msgid "My Profile"
|
||||||
|
msgstr "موقعي الشخصي"
|
||||||
|
|
||||||
|
msgid "Account Settings"
|
||||||
|
msgstr "إعدادات الحساب"
|
||||||
|
|
||||||
|
msgid "Billing & Invoices"
|
||||||
|
msgstr "إدارة الفواتير والضمان"
|
||||||
|
|
||||||
|
msgid "Log Out"
|
||||||
|
msgstr "تسجيل الخروج"
|
||||||
|
|
||||||
|
msgid "Email Address"
|
||||||
|
msgstr "عنوان البريد الإلكتروني"
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "كلمة المرور"
|
||||||
|
|
||||||
|
msgid "Remember me on this device"
|
||||||
|
msgstr "تذكرني على هذا الجهاز"
|
||||||
|
|
||||||
|
msgid "Forgot your password?"
|
||||||
|
msgstr "هل فقدت كلمة المرور؟"
|
||||||
|
|
||||||
|
msgid "Don't have an account? Sign up."
|
||||||
|
msgstr "لا يوجد حساب؟ سجل."
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Ensure this field has at least %(limit_value)d characters (it has "
|
||||||
|
"%(show_value)d)."
|
||||||
|
msgstr ""
|
||||||
|
"تأكد من أن هذا الحقل لديه على الأقل %(limit_value)d من الأحرف (إنه لديه "
|
||||||
|
"%(show_value)d)."
|
||||||
|
|
||||||
|
msgctxt "noun"
|
||||||
|
msgid "Book"
|
||||||
|
msgstr "كتاب"
|
||||||
|
|
||||||
|
msgctxt "verb"
|
||||||
|
msgid "Book"
|
||||||
|
msgstr "كتاب"
|
||||||
|
|
||||||
|
msgctxt "month_name"
|
||||||
|
msgid "May"
|
||||||
|
msgstr "قد"
|
||||||
|
|
||||||
|
msgctxt "auxiliary_verb"
|
||||||
|
msgid "May"
|
||||||
|
msgstr "قد"
|
||||||
|
|
||||||
|
msgid "Product Description"
|
||||||
|
msgstr "وصف المنتج"
|
||||||
|
|
||||||
|
msgid "Add to Cart"
|
||||||
|
msgstr "إضافة إلى عربة التسوق"
|
||||||
|
|
||||||
|
msgid "Proceed to Checkout"
|
||||||
|
msgstr "التوجه إلى الدفع"
|
||||||
|
|
||||||
|
msgid "Total: $%(amount).2f"
|
||||||
|
msgstr "المجموع: $%(amount).2f"
|
||||||
|
|
||||||
|
msgid "Shipping Address"
|
||||||
|
msgstr "عنوان الشحن"
|
||||||
|
|
||||||
|
msgid "Order History"
|
||||||
|
msgstr "إدارة الطلبات"
|
||||||
|
|
||||||
|
msgid "Delete Account"
|
||||||
|
msgstr "حذف الحساب"
|
||||||
|
|
||||||
|
msgid "Save Changes"
|
||||||
|
msgstr "حفظ التغييرات"
|
||||||
|
|
||||||
|
msgid "Upload Avatar"
|
||||||
|
msgstr "تحميل صورة الملف الشخصي"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Welcome to the platform. By using our services, you agree to our <a "
|
||||||
|
"href='%(terms_url)s'>Terms of Service</a> and <a "
|
||||||
|
"href='%(privacy_url)s'>Privacy Policy</a>."
|
||||||
|
msgstr ""
|
||||||
|
"مرحبًا بكم في المنصة. باستخدام خدماتنا، فإنك توافق على <a "
|
||||||
|
"href='%(terms_url)s'>Terms of Service</a> و <a "
|
||||||
|
"href='%(privacy_url)s'>Privacy Policy</a>."
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Please check your email inbox. We have sent a confirmation link to verify "
|
||||||
|
"your account ownership. The link will expire in 24 hours."
|
||||||
|
msgstr ""
|
||||||
|
"يرجى التحقق من صندوق بريدك الإلكتروني. قمنا بإرسال رابط التحقق من ملكية "
|
||||||
|
"حسابك. سيتم انتهاء هذا الرابط في 24 ساعة."
|
||||||
|
|
||||||
|
msgid "<strong>Warning:</strong> This action cannot be undone."
|
||||||
|
msgstr "تحذير: <strong>لا يمكن استعادتها.**"
|
||||||
|
|
||||||
|
msgid "404 - Page Not Found"
|
||||||
|
msgstr "404 - صفحة غير موجودة"
|
||||||
|
|
||||||
|
msgid "Internal Server Error (500)"
|
||||||
|
msgstr "خطأ داخلي (500)"
|
||||||
|
|
||||||
|
msgid "API Connection Timeout"
|
||||||
|
msgstr "وقت انقطاع الاتصال بـ API"
|
||||||
|
|
||||||
|
msgid "Invalid CSRF Token"
|
||||||
|
msgstr "توقيع CSRF غير صالح"
|
||||||
|
|
||||||
|
msgid "Monday"
|
||||||
|
msgstr "الاثنين"
|
||||||
|
|
||||||
|
msgid "Tuesday"
|
||||||
|
msgstr "الثلاثاء"
|
||||||
|
|
||||||
|
msgid "Wednesday"
|
||||||
|
msgstr "الأربعاء"
|
||||||
|
|
||||||
|
msgid "Thursday"
|
||||||
|
msgstr "الخميس"
|
||||||
|
|
||||||
|
msgid "Friday"
|
||||||
|
msgstr "الجمعة"
|
||||||
|
|
||||||
|
msgid "Saturday"
|
||||||
|
msgstr "السبت"
|
||||||
|
|
||||||
|
msgid "Sunday"
|
||||||
|
msgstr "الأحد"
|
||||||
|
|
||||||
|
msgid "Just now"
|
||||||
|
msgstr "الآن"
|
||||||
|
|
||||||
|
msgid "%(count)s minutes ago"
|
||||||
|
msgstr "%(count)s دقائق مضت"
|
||||||
|
|
||||||
|
msgid "Step 1 of 5"
|
||||||
|
msgstr "الخطوة الأولى من 5"
|
||||||
|
|
||||||
|
msgid "Skip tutorial"
|
||||||
|
msgstr "تجاهل التوثيق"
|
||||||
|
|
||||||
|
msgid "Next"
|
||||||
|
msgstr "التالي"
|
||||||
|
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "السابق"
|
||||||
|
|
||||||
|
msgid "Finish"
|
||||||
|
msgstr "الانتهاء"
|
||||||
10252
django1.po
Normal file
10252
django1.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -149,7 +149,7 @@ def candidate_user_required(view_func):
|
|||||||
|
|
||||||
|
|
||||||
def staff_user_required(view_func):
|
def staff_user_required(view_func):
|
||||||
|
|
||||||
"""Decorator to restrict view to staff users only."""
|
"""Decorator to restrict view to staff users only."""
|
||||||
return user_type_required(['staff'])(view_func)
|
return user_type_required(['staff'])(view_func)
|
||||||
|
|
||||||
|
|||||||
@ -310,7 +310,7 @@ class ApplicationForm(forms.ModelForm):
|
|||||||
self.helper.label_class = "col-md-3"
|
self.helper.label_class = "col-md-3"
|
||||||
self.helper.field_class = "col-md-9"
|
self.helper.field_class = "col-md-9"
|
||||||
if current_agency:
|
if current_agency:
|
||||||
# IMPORTANT: Replace 'agency' below with the actual field name
|
# IMPORTANT: Replace 'agency' below with the actual field name
|
||||||
# on your Person model that links it back to the Agency model.
|
# on your Person model that links it back to the Agency model.
|
||||||
self.fields['person'].queryset = self.fields['person'].queryset.filter(
|
self.fields['person'].queryset = self.fields['person'].queryset.filter(
|
||||||
agency=current_agency
|
agency=current_agency
|
||||||
@ -318,7 +318,7 @@ class ApplicationForm(forms.ModelForm):
|
|||||||
self.fields['job'].queryset = self.fields['job'].queryset.filter(
|
self.fields['job'].queryset = self.fields['job'].queryset.filter(
|
||||||
pk=current_job.id
|
pk=current_job.id
|
||||||
)
|
)
|
||||||
self.fields['job'].initial = current_job
|
self.fields['job'].initial = current_job
|
||||||
|
|
||||||
self.fields['job'].widget.attrs['readonly'] = True
|
self.fields['job'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
@ -934,38 +934,34 @@ class HiringAgencyForm(forms.ModelForm):
|
|||||||
"name": forms.TextInput(
|
"name": forms.TextInput(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"placeholder": "Enter agency name",
|
|
||||||
"required": True,
|
"required": True,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"contact_person": forms.TextInput(
|
"contact_person": forms.TextInput(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"placeholder": "Enter contact person name",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"email": forms.EmailInput(
|
"email": forms.EmailInput(
|
||||||
attrs={"class": "form-control", "placeholder": "agency@example.com"}
|
attrs={"class": "form-control"}
|
||||||
),
|
),
|
||||||
"phone": forms.TextInput(
|
"phone": forms.TextInput(
|
||||||
attrs={"class": "form-control", "placeholder": "+966 50 123 4567"}
|
attrs={"class": "form-control"}
|
||||||
),
|
),
|
||||||
"website": forms.URLInput(
|
"website": forms.URLInput(
|
||||||
attrs={"class": "form-control", "placeholder": "https://www.agency.com"}
|
attrs={"class": "form-control"}
|
||||||
),
|
),
|
||||||
"country": forms.Select(attrs={"class": "form-select"}),
|
"country": forms.Select(attrs={"class": "form-select"}),
|
||||||
"address": forms.Textarea(
|
"address": forms.Textarea(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"rows": 3,
|
"rows": 3,
|
||||||
"placeholder": "Enter agency address",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"notes": forms.Textarea(
|
"notes": forms.Textarea(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "form-control",
|
"class": "form-control",
|
||||||
"rows": 3,
|
"rows": 3,
|
||||||
"placeholder": "Internal notes about the agency",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -2490,12 +2486,11 @@ class StaffAssignmentForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Filter users to only show staff members
|
# Filter users to only show staff members
|
||||||
self.fields['assigned_to'].queryset = User.objects.filter(
|
self.fields['assigned_to'].queryset = User.objects.filter(
|
||||||
user_type='staff'
|
user_type='staff',is_superuser=False
|
||||||
).order_by('first_name', 'last_name')
|
).order_by('first_name', 'last_name')
|
||||||
|
|
||||||
# Add empty choice for unassigning
|
# Add empty choice for unassigning
|
||||||
self.fields['assigned_to'].required = False
|
self.fields['assigned_to'].required = False
|
||||||
self.fields['assigned_to'].empty_label = _('-- Unassign Staff --')
|
|
||||||
|
|
||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
self.helper.form_method = 'post'
|
self.helper.form_method = 'post'
|
||||||
@ -2516,3 +2511,4 @@ class StaffAssignmentForm(forms.ModelForm):
|
|||||||
if assigned_to and assigned_to.user_type != 'staff':
|
if assigned_to and assigned_to.user_type != 'staff':
|
||||||
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
|
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
|
||||||
return assigned_to
|
return assigned_to
|
||||||
|
|
||||||
|
|||||||
159
recruitment/management/commands/translate_po1.py
Normal file
159
recruitment/management/commands/translate_po1.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import polib
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from openai import OpenAI, APIConnectionError, RateLimitError
|
||||||
|
|
||||||
|
# Get API Key from settings or environment
|
||||||
|
API_KEY = "8319706a96014c5099b44057d231a154.YfbEMn17ZWXPudxK"
|
||||||
|
# API_KEY = getattr(settings, 'ZAI_API_KEY', os.environ.get('ZAI_API_KEY'))
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Translate or fix fuzzy entries in a .po file using Z.ai (GLM) via OpenAI SDK'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('po_file_path', type=str, help='Path to the .po file')
|
||||||
|
parser.add_argument('--lang', type=str, help='Target language (e.g., "Chinese", "French")', required=True)
|
||||||
|
parser.add_argument('--batch-size', type=int, default=10, help='Entries per API call (default: 10)')
|
||||||
|
parser.add_argument('--workers', type=int, default=3, help='Concurrent threads (default: 3)')
|
||||||
|
parser.add_argument('--model', type=str, default="glm-4.6", help='Model version (default: glm-4.6)')
|
||||||
|
parser.add_argument('--fix-fuzzy', action='store_true', help='Include entries marked as fuzzy')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if not API_KEY:
|
||||||
|
self.stderr.write(self.style.ERROR("Error: ZAI_API_KEY not found in settings or environment."))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Initialize Client based on your docs
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=API_KEY,
|
||||||
|
#base_url="https://api.z.ai/api/paas/v4/"
|
||||||
|
base_url="https://api.z.ai/api/coding/paas/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = options['po_file_path']
|
||||||
|
target_lang = options['lang']
|
||||||
|
batch_size = options['batch_size']
|
||||||
|
max_workers = options['workers']
|
||||||
|
model_name = options['model']
|
||||||
|
fix_fuzzy = options['fix_fuzzy']
|
||||||
|
|
||||||
|
# 2. Load PO File
|
||||||
|
self.stdout.write(f"Loading {file_path}...")
|
||||||
|
try:
|
||||||
|
po = polib.pofile(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
self.stderr.write(self.style.ERROR(f"Could not load file: {e}"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Filter Entries
|
||||||
|
entries_to_process = []
|
||||||
|
for entry in po:
|
||||||
|
if entry.obsolete:
|
||||||
|
continue
|
||||||
|
if not entry.msgstr.strip() or (fix_fuzzy and 'fuzzy' in entry.flags):
|
||||||
|
entries_to_process.append(entry)
|
||||||
|
|
||||||
|
total = len(entries_to_process)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Found {total} entries to process."))
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Batch Processing Logic
|
||||||
|
def chunked(iterable, n):
|
||||||
|
for i in range(0, len(iterable), n):
|
||||||
|
yield iterable[i:i + n]
|
||||||
|
|
||||||
|
batches = list(chunked(entries_to_process, batch_size))
|
||||||
|
self.stdout.write(f"Processing {len(batches)} batches with model {model_name}...")
|
||||||
|
|
||||||
|
# 5. Worker Function with Retry Logic
|
||||||
|
def process_batch(batch_entries):
|
||||||
|
texts = [e.msgid for e in batch_entries]
|
||||||
|
|
||||||
|
system_prompt = (
|
||||||
|
"You are a professional localization expert for a Django software project. "
|
||||||
|
"You will receive a JSON list of English strings. "
|
||||||
|
"Translate them accurately. "
|
||||||
|
"IMPORTANT Rules:\n"
|
||||||
|
"1. Return ONLY a JSON list of strings.\n"
|
||||||
|
"2. Preserve all Python variables (e.g. %(count)s, {name}, %s) exactly.\n"
|
||||||
|
"3. Do not translate HTML tags.\n"
|
||||||
|
"4. Do not explain, just return the JSON."
|
||||||
|
)
|
||||||
|
|
||||||
|
user_prompt = (
|
||||||
|
f"Translate these texts into {target_lang}:\n"
|
||||||
|
f"{json.dumps(texts, ensure_ascii=False)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simple retry loop for Rate Limits
|
||||||
|
attempts = 0
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
|
while attempts < max_retries:
|
||||||
|
try:
|
||||||
|
completion = client.chat.completions.create(
|
||||||
|
model=model_name,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
temperature=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
content = completion.choices[0].message.content
|
||||||
|
# Sanitize markdown code blocks
|
||||||
|
content = content.replace('```json', '').replace('```', '').strip()
|
||||||
|
|
||||||
|
translations = json.loads(content)
|
||||||
|
|
||||||
|
if len(translations) != len(batch_entries):
|
||||||
|
return False, f"Mismatch: sent {len(batch_entries)}, got {len(translations)}"
|
||||||
|
|
||||||
|
# Update entries
|
||||||
|
for entry, trans in zip(batch_entries, translations):
|
||||||
|
entry.msgstr = trans
|
||||||
|
if 'fuzzy' in entry.flags:
|
||||||
|
entry.flags.remove('fuzzy')
|
||||||
|
|
||||||
|
return True, "Success"
|
||||||
|
|
||||||
|
except (RateLimitError, APIConnectionError) as e:
|
||||||
|
attempts += 1
|
||||||
|
wait_time = 2 ** attempts # Exponential backoff: 2s, 4s, 8s...
|
||||||
|
time.sleep(wait_time)
|
||||||
|
if attempts == max_retries:
|
||||||
|
return False, f"API Error after retries: {e}"
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return False, "AI returned invalid JSON"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
# 6. Execution & Incremental Saving
|
||||||
|
success_count = 0
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
future_to_batch = {executor.submit(process_batch, batch): batch for batch in batches}
|
||||||
|
|
||||||
|
for i, future in enumerate(as_completed(future_to_batch)):
|
||||||
|
batch = future_to_batch[future]
|
||||||
|
success, msg = future.result()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
success_count += len(batch)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Batch {i+1}/{len(batches)} done."))
|
||||||
|
else:
|
||||||
|
self.stderr.write(self.style.WARNING(f"Batch {i+1} failed: {msg}"))
|
||||||
|
|
||||||
|
# Save every 5 batches to be safe
|
||||||
|
if (i + 1) % 5 == 0:
|
||||||
|
po.save()
|
||||||
|
self.stdout.write(f"--- Auto-saved at batch {i+1} ---")
|
||||||
|
|
||||||
|
# Final Save
|
||||||
|
po.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"\nComplete! Translated {success_count}/{total} entries."))
|
||||||
@ -129,7 +129,7 @@ class JobPosting(Base):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
application_deadline = models.DateField(db_index=True) # Added index
|
application_deadline = models.DateField(db_index=True)
|
||||||
application_instructions = CKEditor5Field(
|
application_instructions = CKEditor5Field(
|
||||||
blank=True, null=True, config_name="extends"
|
blank=True, null=True, config_name="extends"
|
||||||
)
|
)
|
||||||
@ -912,34 +912,34 @@ class Application(Base):
|
|||||||
@property
|
@property
|
||||||
def get_latest_meeting(self):
|
def get_latest_meeting(self):
|
||||||
"""
|
"""
|
||||||
Retrieves the most specific location details (subclass instance)
|
Retrieves the most specific location details (subclass instance)
|
||||||
of the latest ScheduledInterview for this application, or None.
|
of the latest ScheduledInterview for this application, or None.
|
||||||
"""
|
"""
|
||||||
# 1. Get the latest ScheduledInterview
|
# 1. Get the latest ScheduledInterview
|
||||||
schedule = self.scheduled_interviews.order_by("-created_at").first()
|
schedule = self.scheduled_interviews.order_by("-created_at").first()
|
||||||
|
|
||||||
# Check if a schedule exists and if it has an interview location
|
# Check if a schedule exists and if it has an interview location
|
||||||
if not schedule or not schedule.interview_location:
|
if not schedule or not schedule.interview_location:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get the base location instance
|
# Get the base location instance
|
||||||
interview_location = schedule.interview_location
|
interview_location = schedule.interview_location
|
||||||
|
|
||||||
# 2. Safely retrieve the specific subclass details
|
# 2. Safely retrieve the specific subclass details
|
||||||
|
|
||||||
# Determine the expected subclass accessor name based on the location_type
|
# Determine the expected subclass accessor name based on the location_type
|
||||||
if interview_location.location_type == 'Remote':
|
if interview_location.location_type == 'Remote':
|
||||||
accessor_name = 'zoommeetingdetails'
|
accessor_name = 'zoommeetingdetails'
|
||||||
else: # Assumes 'Onsite' or any other type defaults to Onsite
|
else: # Assumes 'Onsite' or any other type defaults to Onsite
|
||||||
accessor_name = 'onsitelocationdetails'
|
accessor_name = 'onsitelocationdetails'
|
||||||
|
|
||||||
# Use getattr to safely retrieve the specific meeting object (subclass instance).
|
# Use getattr to safely retrieve the specific meeting object (subclass instance).
|
||||||
# If the accessor exists but points to None (because the subclass record was deleted),
|
# If the accessor exists but points to None (because the subclass record was deleted),
|
||||||
# or if the accessor name is wrong for the object's true type, it will return None.
|
# or if the accessor name is wrong for the object's true type, it will return None.
|
||||||
meeting_details = getattr(interview_location, accessor_name, None)
|
meeting_details = getattr(interview_location, accessor_name, None)
|
||||||
|
|
||||||
return meeting_details
|
return meeting_details
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_future_meeting(self):
|
def has_future_meeting(self):
|
||||||
@ -1034,13 +1034,13 @@ class TrainingMaterial(Base):
|
|||||||
|
|
||||||
class InterviewLocation(Base):
|
class InterviewLocation(Base):
|
||||||
"""
|
"""
|
||||||
Base model for all interview location/meeting details (remote or onsite)
|
Base model for all interview location/meeting details (remote or onsite)
|
||||||
using Multi-Table Inheritance.
|
using Multi-Table Inheritance.
|
||||||
"""
|
"""
|
||||||
class LocationType(models.TextChoices):
|
class LocationType(models.TextChoices):
|
||||||
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
|
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
|
||||||
ONSITE = 'Onsite', _('In-Person (Physical Location)')
|
ONSITE = 'Onsite', _('In-Person (Physical Location)')
|
||||||
|
|
||||||
class Status(models.TextChoices):
|
class Status(models.TextChoices):
|
||||||
"""Defines the possible real-time statuses for any interview location/meeting."""
|
"""Defines the possible real-time statuses for any interview location/meeting."""
|
||||||
WAITING = "waiting", _("Waiting")
|
WAITING = "waiting", _("Waiting")
|
||||||
@ -1054,23 +1054,23 @@ class InterviewLocation(Base):
|
|||||||
verbose_name=_("Location Type"),
|
verbose_name=_("Location Type"),
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
details_url = models.URLField(
|
details_url = models.URLField(
|
||||||
verbose_name=_("Meeting/Location URL"),
|
verbose_name=_("Meeting/Location URL"),
|
||||||
max_length=2048,
|
max_length=2048,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
|
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("Location/Meeting Topic"),
|
verbose_name=_("Location/Meeting Topic"),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
|
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
|
||||||
)
|
)
|
||||||
|
|
||||||
timezone = models.CharField(
|
timezone = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_("Timezone"),
|
verbose_name=_("Timezone"),
|
||||||
default='UTC'
|
default='UTC'
|
||||||
)
|
)
|
||||||
@ -1086,7 +1086,7 @@ class InterviewLocation(Base):
|
|||||||
|
|
||||||
class ZoomMeetingDetails(InterviewLocation):
|
class ZoomMeetingDetails(InterviewLocation):
|
||||||
"""Concrete model for remote interviews (Zoom specifics)."""
|
"""Concrete model for remote interviews (Zoom specifics)."""
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
db_index=True,
|
db_index=True,
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@ -1101,7 +1101,7 @@ class ZoomMeetingDetails(InterviewLocation):
|
|||||||
)
|
)
|
||||||
meeting_id = models.CharField(
|
meeting_id = models.CharField(
|
||||||
db_index=True,
|
db_index=True,
|
||||||
max_length=50,
|
max_length=50,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_("External Meeting ID")
|
verbose_name=_("External Meeting ID")
|
||||||
)
|
)
|
||||||
@ -1117,7 +1117,7 @@ class ZoomMeetingDetails(InterviewLocation):
|
|||||||
join_before_host = models.BooleanField(
|
join_before_host = models.BooleanField(
|
||||||
default=False, verbose_name=_("Join Before Host")
|
default=False, verbose_name=_("Join Before Host")
|
||||||
)
|
)
|
||||||
|
|
||||||
host_email=models.CharField(null=True,blank=True)
|
host_email=models.CharField(null=True,blank=True)
|
||||||
mute_upon_entry = models.BooleanField(
|
mute_upon_entry = models.BooleanField(
|
||||||
default=False, verbose_name=_("Mute Upon Entry")
|
default=False, verbose_name=_("Mute Upon Entry")
|
||||||
@ -1137,17 +1137,17 @@ class ZoomMeetingDetails(InterviewLocation):
|
|||||||
|
|
||||||
class OnsiteLocationDetails(InterviewLocation):
|
class OnsiteLocationDetails(InterviewLocation):
|
||||||
"""Concrete model for onsite interviews (Room/Address specifics)."""
|
"""Concrete model for onsite interviews (Room/Address specifics)."""
|
||||||
|
|
||||||
physical_address = models.CharField(
|
physical_address = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("Physical Address"),
|
verbose_name=_("Physical Address"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
room_number = models.CharField(
|
room_number = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_("Room Number/Name"),
|
verbose_name=_("Room Number/Name"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
start_time = models.DateTimeField(
|
start_time = models.DateTimeField(
|
||||||
@ -1181,7 +1181,7 @@ class OnsiteLocationDetails(InterviewLocation):
|
|||||||
|
|
||||||
class InterviewSchedule(Base):
|
class InterviewSchedule(Base):
|
||||||
"""Stores the TEMPLATE criteria for BULK interview generation."""
|
"""Stores the TEMPLATE criteria for BULK interview generation."""
|
||||||
|
|
||||||
# We need a field to store the template location details linked to this bulk schedule.
|
# We need a field to store the template location details linked to this bulk schedule.
|
||||||
# This location object contains the generic Zoom/Onsite info to be cloned.
|
# This location object contains the generic Zoom/Onsite info to be cloned.
|
||||||
template_location = models.ForeignKey(
|
template_location = models.ForeignKey(
|
||||||
@ -1192,17 +1192,17 @@ class InterviewSchedule(Base):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Location Template (Zoom/Onsite)")
|
verbose_name=_("Location Template (Zoom/Onsite)")
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE: schedule_interview_type field is needed in the form,
|
# NOTE: schedule_interview_type field is needed in the form,
|
||||||
# but not on the model itself if we use template_location.
|
# but not on the model itself if we use template_location.
|
||||||
# If you want to keep it:
|
# If you want to keep it:
|
||||||
schedule_interview_type = models.CharField(
|
schedule_interview_type = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=InterviewLocation.LocationType.choices,
|
choices=InterviewLocation.LocationType.choices,
|
||||||
verbose_name=_("Interview Type"),
|
verbose_name=_("Interview Type"),
|
||||||
default=InterviewLocation.LocationType.REMOTE
|
default=InterviewLocation.LocationType.REMOTE
|
||||||
)
|
)
|
||||||
|
|
||||||
job = models.ForeignKey(
|
job = models.ForeignKey(
|
||||||
JobPosting,
|
JobPosting,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -1212,14 +1212,14 @@ class InterviewSchedule(Base):
|
|||||||
applications = models.ManyToManyField(
|
applications = models.ManyToManyField(
|
||||||
Application, related_name="interview_schedules", blank=True
|
Application, related_name="interview_schedules", blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
|
start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
|
||||||
end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
|
end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
|
||||||
|
|
||||||
working_days = models.JSONField(
|
working_days = models.JSONField(
|
||||||
verbose_name=_("Working Days")
|
verbose_name=_("Working Days")
|
||||||
)
|
)
|
||||||
|
|
||||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||||
|
|
||||||
@ -1246,7 +1246,7 @@ class InterviewSchedule(Base):
|
|||||||
|
|
||||||
class ScheduledInterview(Base):
|
class ScheduledInterview(Base):
|
||||||
"""Stores individual scheduled interviews (whether bulk or individually created)."""
|
"""Stores individual scheduled interviews (whether bulk or individually created)."""
|
||||||
|
|
||||||
class InterviewStatus(models.TextChoices):
|
class InterviewStatus(models.TextChoices):
|
||||||
SCHEDULED = "scheduled", _("Scheduled")
|
SCHEDULED = "scheduled", _("Scheduled")
|
||||||
CONFIRMED = "confirmed", _("Confirmed")
|
CONFIRMED = "confirmed", _("Confirmed")
|
||||||
@ -1265,13 +1265,13 @@ class ScheduledInterview(Base):
|
|||||||
related_name="scheduled_interviews",
|
related_name="scheduled_interviews",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Links to the specific, individual location/meeting details for THIS interview
|
# Links to the specific, individual location/meeting details for THIS interview
|
||||||
interview_location = models.OneToOneField(
|
interview_location = models.OneToOneField(
|
||||||
InterviewLocation,
|
InterviewLocation,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="scheduled_interview",
|
related_name="scheduled_interview",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name=_("Meeting/Location Details")
|
verbose_name=_("Meeting/Location Details")
|
||||||
@ -1286,13 +1286,13 @@ class ScheduledInterview(Base):
|
|||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
participants = models.ManyToManyField('Participants', blank=True)
|
participants = models.ManyToManyField('Participants', blank=True)
|
||||||
system_users = models.ManyToManyField(User, related_name="attended_interviews", blank=True)
|
system_users = models.ManyToManyField(User, related_name="attended_interviews", blank=True)
|
||||||
|
|
||||||
interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
|
interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
|
||||||
interview_time = models.TimeField(verbose_name=_("Interview Time"))
|
interview_time = models.TimeField(verbose_name=_("Interview Time"))
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
db_index=True,
|
db_index=True,
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@ -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):
|
||||||
@ -1320,7 +1331,7 @@ class InterviewNote(Base):
|
|||||||
FEEDBACK = 'Feedback', _('Candidate Feedback')
|
FEEDBACK = 'Feedback', _('Candidate Feedback')
|
||||||
LOGISTICS = 'Logistics', _('Logistical Note')
|
LOGISTICS = 'Logistics', _('Logistical Note')
|
||||||
GENERAL = 'General', _('General Comment')
|
GENERAL = 'General', _('General Comment')
|
||||||
|
|
||||||
1
|
1
|
||||||
interview = models.ForeignKey(
|
interview = models.ForeignKey(
|
||||||
ScheduledInterview,
|
ScheduledInterview,
|
||||||
@ -1329,7 +1340,7 @@ class InterviewNote(Base):
|
|||||||
verbose_name=_("Scheduled Interview"),
|
verbose_name=_("Scheduled Interview"),
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -1337,16 +1348,16 @@ class InterviewNote(Base):
|
|||||||
verbose_name=_("Author"),
|
verbose_name=_("Author"),
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
note_type = models.CharField(
|
note_type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=NoteType.choices,
|
choices=NoteType.choices,
|
||||||
default=NoteType.FEEDBACK,
|
default=NoteType.FEEDBACK,
|
||||||
verbose_name=_("Note Type")
|
verbose_name=_("Note Type")
|
||||||
)
|
)
|
||||||
|
|
||||||
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
|
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Interview Note")
|
verbose_name = _("Interview Note")
|
||||||
verbose_name_plural = _("Interview Notes")
|
verbose_name_plural = _("Interview Notes")
|
||||||
@ -1354,7 +1365,7 @@ class InterviewNote(Base):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
|
return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
|
||||||
|
|
||||||
|
|
||||||
class FormTemplate(Base):
|
class FormTemplate(Base):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -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,
|
||||||
@ -660,5 +665,9 @@ 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"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -129,9 +129,9 @@ from .models import (
|
|||||||
Source,
|
Source,
|
||||||
Message,
|
Message,
|
||||||
Document,
|
Document,
|
||||||
OnsiteLocationDetails,
|
|
||||||
InterviewLocation,
|
InterviewLocation,
|
||||||
InterviewNote
|
InterviewNote,
|
||||||
|
OnsiteLocationDetails
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -162,11 +162,11 @@ class PersonListView(StaffRequiredMixin, ListView):
|
|||||||
gender=self.request.GET.get('gender')
|
gender=self.request.GET.get('gender')
|
||||||
if gender:
|
if gender:
|
||||||
queryset=queryset.filter(gender=gender)
|
queryset=queryset.filter(gender=gender)
|
||||||
|
|
||||||
nationality=self.request.GET.get('nationality')
|
nationality=self.request.GET.get('nationality')
|
||||||
if nationality:
|
if nationality:
|
||||||
queryset=queryset.filter(nationality=nationality)
|
queryset=queryset.filter(nationality=nationality)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context=super().get_context_data(**kwargs)
|
context=super().get_context_data(**kwargs)
|
||||||
@ -174,7 +174,7 @@ class PersonListView(StaffRequiredMixin, ListView):
|
|||||||
nationalities = self.model.objects.values_list('nationality', flat=True).filter(
|
nationalities = self.model.objects.values_list('nationality', flat=True).filter(
|
||||||
nationality__isnull=False
|
nationality__isnull=False
|
||||||
).distinct().order_by('nationality')
|
).distinct().order_by('nationality')
|
||||||
|
|
||||||
nationality=self.request.GET.get('nationality')
|
nationality=self.request.GET.get('nationality')
|
||||||
context['nationality']=nationality
|
context['nationality']=nationality
|
||||||
context['nationalities']=nationalities
|
context['nationalities']=nationalities
|
||||||
@ -212,12 +212,17 @@ class PersonDetailView(DetailView):
|
|||||||
context_object_name = "person"
|
context_object_name = "person"
|
||||||
|
|
||||||
|
|
||||||
class PersonUpdateView(StaffRequiredMixin, UpdateView):
|
class PersonUpdateView( UpdateView):
|
||||||
model = Person
|
model = Person
|
||||||
template_name = "people/update_person.html"
|
template_name = "people/update_person.html"
|
||||||
form_class = PersonForm
|
form_class = PersonForm
|
||||||
success_url = reverse_lazy("person_list")
|
success_url = reverse_lazy("person_list")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if self.request.POST.get("view") == "portal":
|
||||||
|
form.save()
|
||||||
|
return redirect("agency_portal_persons_list")
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
class PersonDeleteView(StaffRequiredMixin, DeleteView):
|
class PersonDeleteView(StaffRequiredMixin, DeleteView):
|
||||||
model = Person
|
model = Person
|
||||||
@ -615,6 +620,7 @@ def job_detail(request, slug):
|
|||||||
"avg_t2i_days": avg_t2i_days,
|
"avg_t2i_days": avg_t2i_days,
|
||||||
"avg_t_in_exam_days": avg_t_in_exam_days,
|
"avg_t_in_exam_days": avg_t_in_exam_days,
|
||||||
"linkedin_content_form": linkedin_content_form,
|
"linkedin_content_form": linkedin_content_form,
|
||||||
|
"staff_form": StaffAssignmentForm(),
|
||||||
}
|
}
|
||||||
return render(request, "jobs/job_detail.html", context)
|
return render(request, "jobs/job_detail.html", context)
|
||||||
|
|
||||||
@ -625,7 +631,7 @@ def job_detail(request, slug):
|
|||||||
# def job_cvs_download(request, slug):
|
# def job_cvs_download(request, slug):
|
||||||
# job = get_object_or_404(JobPosting, slug=slug)
|
# job = get_object_or_404(JobPosting, slug=slug)
|
||||||
# entries = Application.objects.filter(job=job)
|
# entries = Application.objects.filter(job=job)
|
||||||
|
|
||||||
# # 2. Create an in-memory byte stream (BytesIO)
|
# # 2. Create an in-memory byte stream (BytesIO)
|
||||||
# zip_buffer = io.BytesIO()
|
# zip_buffer = io.BytesIO()
|
||||||
|
|
||||||
@ -681,7 +687,7 @@ def request_cvs_download(request, slug):
|
|||||||
# Use async_task to run the function in the background
|
# Use async_task to run the function in the background
|
||||||
# Pass only simple arguments (like the job ID)
|
# Pass only simple arguments (like the job ID)
|
||||||
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
|
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
|
||||||
|
|
||||||
# Provide user feedback and redirect
|
# Provide user feedback and redirect
|
||||||
messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.")
|
messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.")
|
||||||
return redirect('job_detail', slug=slug) # Redirect back to the job detail page
|
return redirect('job_detail', slug=slug) # Redirect back to the job detail page
|
||||||
@ -701,7 +707,7 @@ def download_ready_cvs(request, slug):
|
|||||||
# File is not ready or doesn't exist
|
# File is not ready or doesn't exist
|
||||||
messages.warning(request, "The ZIP file is still being generated or an error occurred.")
|
messages.warning(request, "The ZIP file is still being generated or an error occurred.")
|
||||||
return redirect('job_detail', slug=slug)
|
return redirect('job_detail', slug=slug)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def job_image_upload(request, slug):
|
def job_image_upload(request, slug):
|
||||||
@ -1963,8 +1969,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,
|
||||||
@ -1974,8 +1978,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):
|
||||||
"""
|
"""
|
||||||
@ -2991,10 +2993,11 @@ def staff_assignment_view(request, slug):
|
|||||||
applications = job.applications.all()
|
applications = job.applications.all()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = StaffAssignmentForm(request.POST, instance=job)
|
form = StaffAssignmentForm(request.POST, instance=job)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
assignment = form.save()
|
job.assigned_to = form.cleaned_data["assigned_to"]
|
||||||
|
job.save(update_fields=["assigned_to"])
|
||||||
messages.success(request, f"Staff assigned to job '{job.title}' successfully!")
|
messages.success(request, f"Staff assigned to job '{job.title}' successfully!")
|
||||||
return redirect("job_detail", slug=job.slug)
|
return redirect("job_detail", slug=job.slug)
|
||||||
else:
|
else:
|
||||||
@ -3089,7 +3092,8 @@ def add_meeting_comment(request, slug):
|
|||||||
"""Add a comment to a meeting"""
|
"""Add a comment to a meeting"""
|
||||||
# from .forms import MeetingCommentForm
|
# from .forms import MeetingCommentForm
|
||||||
|
|
||||||
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
|
meeting = get_object_or_404(InterviewNote, slug=slug)
|
||||||
|
print(meeting)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = InterviewNoteForm(request.POST)
|
form = InterviewNoteForm(request.POST)
|
||||||
@ -3289,8 +3293,8 @@ def agency_create(request):
|
|||||||
|
|
||||||
context = {
|
context = {
|
||||||
"form": form,
|
"form": form,
|
||||||
"title": "Create New Agency",
|
"title": _("Create New Agency"),
|
||||||
"button_text": "Create Agency",
|
"button_text": _("Create Agency"),
|
||||||
}
|
}
|
||||||
return render(request, "recruitment/agency_form.html", context)
|
return render(request, "recruitment/agency_form.html", context)
|
||||||
|
|
||||||
@ -3347,7 +3351,7 @@ def agency_update(request, slug):
|
|||||||
"form": form,
|
"form": form,
|
||||||
"agency": agency,
|
"agency": agency,
|
||||||
"title": f"Edit Agency: {agency.name}",
|
"title": f"Edit Agency: {agency.name}",
|
||||||
"button_text": "Update Agency",
|
"button_text": _("Update Agency"),
|
||||||
}
|
}
|
||||||
return render(request, "recruitment/agency_form.html", context)
|
return render(request, "recruitment/agency_form.html", context)
|
||||||
|
|
||||||
@ -4094,11 +4098,16 @@ def candidate_application_detail(request, slug):
|
|||||||
return redirect("account_login")
|
return redirect("account_login")
|
||||||
|
|
||||||
# Get candidate profile (Person record)
|
# Get candidate profile (Person record)
|
||||||
try:
|
agency = getattr(request.user,"agency_profile",None)
|
||||||
candidate = request.user.person_profile
|
if agency:
|
||||||
except:
|
candidate = get_object_or_404(Application,slug=slug)
|
||||||
messages.error(request, "No candidate profile found.")
|
# if Application.objects.filter(person=candidate,hirin).exists()
|
||||||
return redirect("account_login")
|
else:
|
||||||
|
try:
|
||||||
|
candidate = request.user.person_profile
|
||||||
|
except:
|
||||||
|
messages.error(request, "No candidate profile found.")
|
||||||
|
return redirect("account_login")
|
||||||
|
|
||||||
# Get the specific application and verify it belongs to this candidate
|
# Get the specific application and verify it belongs to this candidate
|
||||||
application = get_object_or_404(
|
application = get_object_or_404(
|
||||||
@ -4108,7 +4117,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
|
||||||
@ -4674,11 +4683,11 @@ def message_create(request):
|
|||||||
message.sender = request.user
|
message.sender = request.user
|
||||||
message.save()
|
message.save()
|
||||||
# Send email if message_type is 'email' and recipient has email
|
# Send email if message_type is 'email' and recipient has email
|
||||||
|
|
||||||
if message.recipient and message.recipient.email:
|
if message.recipient and message.recipient.email:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
|
||||||
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
||||||
subject=message.subject,
|
subject=message.subject,
|
||||||
@ -4691,23 +4700,23 @@ def message_create(request):
|
|||||||
if email_result:
|
if email_result:
|
||||||
messages.success(request, "Message sent successfully via email!")
|
messages.success(request, "Message sent successfully via email!")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
|
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
||||||
messages.warning(request, f"Message saved but email sending failed: {str(e)}")
|
messages.warning(request, f"Message saved but email sending failed: {str(e)}")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
messages.success(request, "Message sent successfully!")
|
messages.success(request, "Message sent successfully!")
|
||||||
|
|
||||||
|
|
||||||
return redirect("message_list")
|
return redirect("message_list")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
messages.error(request, "Please correct the errors below.")
|
messages.error(request, "Please correct the errors below.")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
form = MessageForm(request.user)
|
form = MessageForm(request.user)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
@ -4757,7 +4766,7 @@ def message_reply(request, message_id):
|
|||||||
if email_result:
|
if email_result:
|
||||||
messages.success(request, "Message sent successfully via email!")
|
messages.success(request, "Message sent successfully via email!")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
|
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -4987,7 +4996,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":
|
||||||
@ -5268,7 +5277,7 @@ def compose_candidate_email(request, job_slug):
|
|||||||
from_interview=False,
|
from_interview=False,
|
||||||
job=job
|
job=job
|
||||||
)
|
)
|
||||||
|
|
||||||
if email_result["success"]:
|
if email_result["success"]:
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if hasattr(candidate, 'person') and candidate.person:
|
if hasattr(candidate, 'person') and candidate.person:
|
||||||
@ -5660,7 +5669,7 @@ def send_interview_email(request, slug):
|
|||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
job=job,
|
job=job,
|
||||||
)
|
)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
# 4. Extract cleaned data
|
# 4. Extract cleaned data
|
||||||
subject = form.cleaned_data["subject"]
|
subject = form.cleaned_data["subject"]
|
||||||
@ -5734,11 +5743,11 @@ def send_interview_email(request, slug):
|
|||||||
)
|
)
|
||||||
return redirect("list_meetings")
|
return redirect("list_meetings")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
error_msg = "Failed to send email. Please check the form for errors."
|
error_msg = "Failed to send email. Please check the form for errors."
|
||||||
print(form.errors)
|
print(form.errors)
|
||||||
messages.error(request, error_msg)
|
messages.error(request, error_msg)
|
||||||
return redirect("meeting_details", slug=meeting.slug)
|
return redirect("meeting_details", slug=meeting.slug)
|
||||||
return redirect("meeting_details", slug=meeting.slug)
|
return redirect("meeting_details", slug=meeting.slug)
|
||||||
|
|
||||||
|
|
||||||
@ -5761,7 +5770,7 @@ class MeetingListView(ListView):
|
|||||||
template_name = "meetings/list_meetings.html"
|
template_name = "meetings/list_meetings.html"
|
||||||
context_object_name = "meetings"
|
context_object_name = "meetings"
|
||||||
paginate_by = 100
|
paginate_by = 100
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Start with a base queryset, ensuring an InterviewLocation link exists.
|
# Start with a base queryset, ensuring an InterviewLocation link exists.
|
||||||
@ -5776,7 +5785,7 @@ class MeetingListView(ListView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Note: Printing the queryset here can consume memory for large sets.
|
# Note: Printing the queryset here can consume memory for large sets.
|
||||||
|
|
||||||
# Get filters from GET request
|
# Get filters from GET request
|
||||||
search_query = self.request.GET.get("q")
|
search_query = self.request.GET.get("q")
|
||||||
status_filter = self.request.GET.get("status")
|
status_filter = self.request.GET.get("status")
|
||||||
@ -5788,11 +5797,11 @@ class MeetingListView(ListView):
|
|||||||
if type_filter:
|
if type_filter:
|
||||||
# Use .title() to handle case variations from URL (e.g., 'remote' -> 'Remote')
|
# Use .title() to handle case variations from URL (e.g., 'remote' -> 'Remote')
|
||||||
normalized_type = type_filter.title()
|
normalized_type = type_filter.title()
|
||||||
|
|
||||||
# Assuming InterviewLocation.LocationType is accessible/defined
|
# Assuming InterviewLocation.LocationType is accessible/defined
|
||||||
if normalized_type in ['Remote', 'Onsite']:
|
if normalized_type in ['Remote', 'Onsite']:
|
||||||
queryset = queryset.filter(interview_location__location_type=normalized_type)
|
queryset = queryset.filter(interview_location__location_type=normalized_type)
|
||||||
|
|
||||||
# 3. Search by Topic (stored on InterviewLocation)
|
# 3. Search by Topic (stored on InterviewLocation)
|
||||||
if search_query:
|
if search_query:
|
||||||
queryset = queryset.filter(interview_location__topic__icontains=search_query)
|
queryset = queryset.filter(interview_location__topic__icontains=search_query)
|
||||||
@ -5874,8 +5883,8 @@ class MeetingListView(ListView):
|
|||||||
# model = InterviewLocation
|
# model = InterviewLocation
|
||||||
# template_name = "meetings/list_meetings.html"
|
# template_name = "meetings/list_meetings.html"
|
||||||
# context_object_name = "meetings"
|
# context_object_name = "meetings"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# def get_queryset(self):
|
# def get_queryset(self):
|
||||||
# # Start with a base queryset, ensuring an InterviewLocation link exists.
|
# # Start with a base queryset, ensuring an InterviewLocation link exists.
|
||||||
@ -5886,7 +5895,7 @@ class MeetingListView(ListView):
|
|||||||
# print(queryset)
|
# print(queryset)
|
||||||
|
|
||||||
# return queryset
|
# return queryset
|
||||||
|
|
||||||
|
|
||||||
def reschedule_onsite_meeting(request, slug, candidate_id, meeting_id):
|
def reschedule_onsite_meeting(request, slug, candidate_id, meeting_id):
|
||||||
"""Handles the rescheduling of an Onsite Interview (updates OnsiteLocationDetails)."""
|
"""Handles the rescheduling of an Onsite Interview (updates OnsiteLocationDetails)."""
|
||||||
@ -6106,3 +6115,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)
|
||||||
|
|||||||
268
run.py
268
run.py
@ -1,44 +1,246 @@
|
|||||||
import requests
|
import os
|
||||||
import jwt
|
import json
|
||||||
import time
|
import time
|
||||||
|
import argparse
|
||||||
|
import polib
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from openai import OpenAI, APIConnectionError, RateLimitError
|
||||||
|
|
||||||
ZOOM_API_KEY = 'OoDRW3uVTymEnnQWTZTsLQ'
|
# --- Terminal Colors ---
|
||||||
ZOOM_API_SECRET = 'ZJ0hCFMrwekG71jbR3Trvoor4tK3HAVP'
|
class Colors:
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
|
||||||
def generate_zoom_jwt():
|
def print_success(msg): print(f"{Colors.GREEN}{msg}{Colors.ENDC}")
|
||||||
payload = {
|
def print_warning(msg): print(f"{Colors.WARNING}{msg}{Colors.ENDC}")
|
||||||
'iss': ZOOM_API_KEY,
|
def print_error(msg): print(f"{Colors.FAIL}{msg}{Colors.ENDC}")
|
||||||
'exp': time.time() + 3600
|
def print_info(msg): print(f"{Colors.BLUE}{msg}{Colors.ENDC}")
|
||||||
}
|
|
||||||
token = jwt.encode(payload, ZOOM_API_SECRET, algorithm='HS256')
|
|
||||||
return token
|
|
||||||
|
|
||||||
def create_zoom_meeting(topic, start_time, duration, host_email):
|
# --- Provider Configurations ---
|
||||||
jwt_token = generate_zoom_jwt()
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'Bearer {jwt_token}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
"topic": topic,
|
|
||||||
"type": 2,
|
|
||||||
"start_time": start_time,
|
|
||||||
"duration": duration,
|
|
||||||
"schedule_for": host_email,
|
|
||||||
"settings": {"join_before_host": True}
|
|
||||||
}
|
|
||||||
url = f"https://api.zoom.us/v2/users/{host_email}/meetings"
|
|
||||||
return requests.post(url, json=data, headers=headers)
|
|
||||||
|
|
||||||
|
class ProviderFactory:
|
||||||
|
@staticmethod
|
||||||
|
def get_client(provider_name, api_key=None, base_url=None):
|
||||||
|
"""
|
||||||
|
Returns a configured OpenAI client and default model based on the provider.
|
||||||
|
"""
|
||||||
|
provider_name = provider_name.lower()
|
||||||
|
|
||||||
|
if provider_name == 'glm':
|
||||||
|
return OpenAI(
|
||||||
|
api_key=api_key or os.environ.get('ZAI_API_KEY'),
|
||||||
|
base_url="https://api.z.ai/api/coding/paas/v4/"
|
||||||
|
), "glm-4.6"
|
||||||
|
|
||||||
|
elif provider_name == 'ollama':
|
||||||
|
# Ollama acts like OpenAI but locally
|
||||||
|
return OpenAI(
|
||||||
|
api_key='ollama', # Required but ignored by Ollama
|
||||||
|
base_url=base_url or "http://localhost:11434/v1"
|
||||||
|
), "llama3" # Default model, user can override
|
||||||
|
|
||||||
|
elif provider_name == 'openai':
|
||||||
|
return OpenAI(
|
||||||
|
api_key=api_key or os.environ.get('OPENAI_API_KEY')
|
||||||
|
), "gpt-4o-mini"
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown provider: {provider_name}")
|
||||||
|
|
||||||
|
# --- Main Logic ---
|
||||||
|
|
||||||
|
def translate_po_file(args):
|
||||||
|
|
||||||
|
# 1. Setup Provider
|
||||||
|
try:
|
||||||
|
client, default_model = ProviderFactory.get_client(args.provider, args.api_key, args.api_base)
|
||||||
|
model_name = args.model or default_model
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Configuration Error: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Load PO File
|
||||||
|
print_info(f"Loading file: {args.path}")
|
||||||
|
try:
|
||||||
|
po = polib.pofile(args.path)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Could not load file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Filter Entries
|
||||||
|
entries_to_process = []
|
||||||
|
for entry in po:
|
||||||
|
if entry.obsolete:
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_empty = not entry.msgstr.strip()
|
||||||
|
is_fuzzy = 'fuzzy' in entry.flags
|
||||||
|
|
||||||
|
if is_empty or (args.fix_fuzzy and is_fuzzy):
|
||||||
|
entries_to_process.append(entry)
|
||||||
|
|
||||||
|
total_entries = len(entries_to_process)
|
||||||
|
print_success(f"Target: {args.lang} | Provider: {args.provider} | Model: {model_name}")
|
||||||
|
print_success(f"Found {total_entries} entries to translate.")
|
||||||
|
|
||||||
|
if total_entries == 0:
|
||||||
|
print_success("Nothing to do.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Special Handling for Local Ollama
|
||||||
|
# Local models struggle with concurrent requests. Force workers=1 unless overridden.
|
||||||
|
if args.provider == 'ollama' and args.workers > 1:
|
||||||
|
print_warning("⚠️ Warning: Using multiple workers with Ollama can crash local GPUs.")
|
||||||
|
print_warning(" Switching to workers=1 for stability (use --workers N to override).")
|
||||||
|
args.workers = 1
|
||||||
|
|
||||||
|
# 5. Batch Processing Helper
|
||||||
|
def chunked(iterable, n):
|
||||||
|
for i in range(0, len(iterable), n):
|
||||||
|
yield iterable[i:i + n]
|
||||||
|
|
||||||
|
batches = list(chunked(entries_to_process, args.batch_size))
|
||||||
|
|
||||||
|
# 6. Worker Function
|
||||||
|
def process_batch(batch_entries):
|
||||||
|
texts = [e.msgid for e in batch_entries]
|
||||||
|
|
||||||
|
# System prompt: Critical for JSON enforcement
|
||||||
|
system_prompt = (
|
||||||
|
"You are a professional technical translator. "
|
||||||
|
"You will receive a JSON list of English strings. "
|
||||||
|
"Translate them accurately. "
|
||||||
|
"IMPORTANT Rules:\n"
|
||||||
|
"1. Return ONLY a valid JSON list of strings.\n"
|
||||||
|
"2. Preserve python formatting (%(count)s, {name}, %s) exactly.\n"
|
||||||
|
"3. Do not translate HTML tags.\n"
|
||||||
|
"4. Do NOT output markdown (like ```json), just raw JSON."
|
||||||
|
)
|
||||||
|
|
||||||
|
user_prompt = (
|
||||||
|
f"Translate these texts into {args.lang}:\n"
|
||||||
|
f"{json.dumps(texts, ensure_ascii=False)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
attempts = 0
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
|
while attempts < max_retries:
|
||||||
|
try:
|
||||||
|
completion = client.chat.completions.create(
|
||||||
|
model=model_name,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
temperature=0.1, # Low temp for deterministic results
|
||||||
|
response_format={"type": "json_object"} if args.provider == "openai" else None
|
||||||
|
)
|
||||||
|
|
||||||
|
content = completion.choices[0].message.content
|
||||||
|
|
||||||
|
# Cleanup: Some local models love adding markdown blocks despite instructions
|
||||||
|
content = content.replace('```json', '').replace('```', '').strip()
|
||||||
|
|
||||||
|
# Flexible JSON parsing
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
# Handle cases where model returns {"translations": [...]} instead of [...]
|
||||||
|
if isinstance(data, dict):
|
||||||
|
# Look for the first list value
|
||||||
|
found_list = False
|
||||||
|
for v in data.values():
|
||||||
|
if isinstance(v, list):
|
||||||
|
translations = v
|
||||||
|
found_list = True
|
||||||
|
break
|
||||||
|
if not found_list:
|
||||||
|
return False, f"Could not find list in JSON object: {data}"
|
||||||
|
else:
|
||||||
|
translations = data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return False, f"Invalid JSON received: {content[:50]}..."
|
||||||
|
|
||||||
|
if not isinstance(translations, list) or len(translations) != len(batch_entries):
|
||||||
|
return False, f"Count mismatch: sent {len(batch_entries)}, got {len(translations) if isinstance(translations, list) else 'invalid'}"
|
||||||
|
|
||||||
|
# Apply translations with Type Checking
|
||||||
|
for entry, translation in zip(batch_entries, translations):
|
||||||
|
|
||||||
|
# --- VITAL FIX FOR AttributeError: 'dict' object has no attribute 'splitlines' ---
|
||||||
|
if isinstance(translation, dict):
|
||||||
|
# Tries to grab the first string value found in the dict (e.g., {"text": "Hello"})
|
||||||
|
extracted = next((str(v) for v in translation.values() if isinstance(v, str)), None)
|
||||||
|
translation = extracted if extracted else str(translation)
|
||||||
|
elif not isinstance(translation, str):
|
||||||
|
# Ensure all other types (like boolean or int) are converted to string
|
||||||
|
translation = str(translation)
|
||||||
|
# ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
entry.msgstr = translation
|
||||||
|
if 'fuzzy' in entry.flags:
|
||||||
|
entry.flags.remove('fuzzy')
|
||||||
|
|
||||||
|
return True, "Success"
|
||||||
|
|
||||||
|
except (RateLimitError, APIConnectionError) as e:
|
||||||
|
attempts += 1
|
||||||
|
time.sleep(2 ** attempts) # Exponential backoff
|
||||||
|
if attempts == max_retries:
|
||||||
|
return False, f"API Error: {e}"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Unexpected: {e}"
|
||||||
|
|
||||||
|
# 7. Execution Loop
|
||||||
|
success_count = 0
|
||||||
|
print_info(f"Starting processing {len(batches)} batches...")
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=args.workers) as executor:
|
||||||
|
future_to_batch = {executor.submit(process_batch, batch): batch for batch in batches}
|
||||||
|
|
||||||
|
for i, future in enumerate(as_completed(future_to_batch)):
|
||||||
|
batch = future_to_batch[future]
|
||||||
|
success, msg = future.result()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
success_count += len(batch)
|
||||||
|
print_success(f"[{i+1}/{len(batches)}] Batch done.")
|
||||||
|
else:
|
||||||
|
print_warning(f"[{i+1}/{len(batches)}] Batch failed: {msg}")
|
||||||
|
|
||||||
|
# Auto-save every 5 batches
|
||||||
|
if (i + 1) % 5 == 0:
|
||||||
|
po.save()
|
||||||
|
|
||||||
|
# 8. Final Save
|
||||||
|
po.save()
|
||||||
|
print_success(f"\n------------------------------------------------")
|
||||||
|
print_success(f"Finished! Translated {success_count}/{total_entries} entries.")
|
||||||
|
print_success(f"File saved: {args.path}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
topic = "Your Meeting Topic"
|
parser = argparse.ArgumentParser(description="Translate .po files using AI Providers (Z.ai, Ollama, OpenAI)")
|
||||||
start_time = "2023-08-25T10:00:00"
|
|
||||||
duration = 60
|
|
||||||
host_email = "your_zoom_email"
|
|
||||||
response = create_zoom_meeting(topic, start_time, duration, host_email)
|
|
||||||
print(response.json())
|
|
||||||
|
|
||||||
|
parser.add_argument('path', type=str, help='Path to the .po file')
|
||||||
|
parser.add_argument('--lang', type=str, required=True, help='Target language (e.g., "French", "zh-CN")')
|
||||||
|
|
||||||
|
# Provider Settings
|
||||||
|
parser.add_argument('--provider', type=str, default='glm', choices=['glm', 'ollama', 'openai'], help='AI Provider to use')
|
||||||
|
parser.add_argument('--model', type=str, help='Model name (e.g., glm-4, llama3, gpt-4). Defaults vary by provider.')
|
||||||
|
parser.add_argument('--api-key', type=str, help='API Key (optional if env var is set)')
|
||||||
|
parser.add_argument('--api-base', type=str, help='Custom API Base URL (useful for custom Ollama ports)')
|
||||||
|
|
||||||
|
# Performance Settings
|
||||||
|
parser.add_argument('--batch-size', type=int, default=10, help='Lines per request. Keep low (5-10) for local models.')
|
||||||
|
parser.add_argument('--workers', type=int, default=3, help='Parallel threads. Note: Ollama defaults to 1.')
|
||||||
|
|
||||||
|
# Logic Settings
|
||||||
|
parser.add_argument('--fix-fuzzy', action='store_true', help='Re-translate entries marked as fuzzy')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
translate_po_file(args)
|
||||||
@ -70,15 +70,9 @@
|
|||||||
|
|
||||||
{# Language and Profile Controls (Keep outside collapse for mobile access) #}
|
{# Language and Profile Controls (Keep outside collapse for mobile access) #}
|
||||||
<div class="d-flex align-items-center order-lg-3">
|
<div class="d-flex align-items-center order-lg-3">
|
||||||
<ul class="navbar-nav flex-row">
|
{% comment %} <ul class="navbar-nav flex-row">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item me-2">
|
||||||
<a class="language-toggle-btn dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
<li class="nav-item me-2">
|
||||||
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
|
||||||
<i class="fas fa-globe"></i>
|
|
||||||
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" data-bs-popper="static">
|
|
||||||
<li>
|
|
||||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
|
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
|
||||||
@ -86,7 +80,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="nav-item me-2">
|
||||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
|
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
|
||||||
@ -96,7 +90,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul> {% endcomment %}
|
||||||
|
|
||||||
<ul class="navbar-nav ms-2 ms-lg-4">
|
<ul class="navbar-nav ms-2 ms-lg-4">
|
||||||
<!-- Notification Bell for Admin Users -->
|
<!-- Notification Bell for Admin Users -->
|
||||||
@ -122,11 +116,33 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %} {% endcomment %}
|
{% endif %} {% endcomment %}
|
||||||
<li class="nav-item me-2">
|
{% comment %} <li class="nav-item me-2">
|
||||||
<a class="nav-link" href="{% url 'message_list' %}">
|
<a class="nav-link" href="{% url 'message_list' %}">
|
||||||
<i class="fas fa-envelope"></i>
|
<i class="fas fa-envelope"></i>
|
||||||
</a>
|
</a>
|
||||||
|
</li> {% endcomment %}
|
||||||
|
<li class="nav-item me-2">
|
||||||
|
{% if LANGUAGE_CODE == 'en' %}
|
||||||
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
|
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
||||||
|
<span class="me-2">🇸🇦</span> العربية
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% elif LANGUAGE_CODE == 'ar' %}
|
||||||
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
|
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
||||||
|
<span class="me-2">🇺🇸</span> English
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item me-2">
|
||||||
|
<a class="nav-link text-white" href="{% url 'message_list' %}">
|
||||||
|
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<button
|
<button
|
||||||
@ -144,12 +160,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;"
|
||||||
>
|
>
|
||||||
@ -215,10 +231,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>
|
||||||
@ -230,11 +242,10 @@
|
|||||||
{# Main Navigation Links (This collapses on mobile) - order-lg-1 ensures it is centered on desktop #}
|
{# Main Navigation Links (This collapses on mobile) - order-lg-1 ensures it is centered on desktop #}
|
||||||
<div class="collapse navbar-collapse order-lg-1" id="navbarNav">
|
<div class="collapse navbar-collapse order-lg-1" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
{# Changed me-4 to me-lg-4 so they stack tightly on mobile #}
|
|
||||||
<li class="nav-item me-lg-4">
|
<li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'job_list' %}active{% endif %}" href="{% url 'job_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'job_list' %}active{% endif %}" href="{% url 'job_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
{% include "icons/jobs.html" %}
|
<i class="fas fa-briefcase me-2"></i>
|
||||||
{% trans "Jobs" %}
|
{% trans "Jobs" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -242,7 +253,7 @@
|
|||||||
<li class="nav-item me-lg-4">
|
<li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'candidate_list' %}active{% endif %}" href="{% url 'candidate_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'candidate_list' %}active{% endif %}" href="{% url 'candidate_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
{% include "icons/users.html" %}
|
<i class="fas fa-user-tie me-2"></i>
|
||||||
{% trans "Applications" %}
|
{% trans "Applications" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -250,7 +261,7 @@
|
|||||||
<li class="nav-item me-lg-4">
|
<li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'person_list' %}active{% endif %}" href="{% url 'person_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'person_list' %}active{% endif %}" href="{% url 'person_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
{% include "icons/users.html" %}
|
<i class="fas fa-user me-2"></i>
|
||||||
{% trans "Applicant" %}
|
{% trans "Applicant" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -258,7 +269,7 @@
|
|||||||
<li class="nav-item me-lg-4">
|
<li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'agency_list' %}active{% endif %}" href="{% url 'agency_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'agency_list' %}active{% endif %}" href="{% url 'agency_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
<i class="fas fa-building"></i>
|
<i class="fas fa-building me-2"></i>
|
||||||
{% trans "Agencies" %}
|
{% trans "Agencies" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -266,9 +277,7 @@
|
|||||||
<li class="nav-item me-lg-4">
|
<li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
<i class="fas fa-calendar-check me-2"></i>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
|
|
||||||
</svg>
|
|
||||||
{% trans "Meetings" %}
|
{% trans "Meetings" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -283,14 +292,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li> {% endcomment %}
|
</li> {% endcomment %}
|
||||||
<li class="nav-item me-lg-4">
|
{% comment %} <li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'participants_list' %}active{% endif %}" href="{% url 'participants_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'participants_list' %}active{% endif %}" href="{% url 'participants_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
{% trans "Participants" %}
|
{% trans "Participants" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n static %}
|
{% load i18n static crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
||||||
|
|
||||||
@ -183,13 +183,13 @@
|
|||||||
|
|
||||||
{# Status badge #}
|
{# Status badge #}
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="status-badge
|
<span class='status-badge
|
||||||
{% if job.status == "ACTIVE" %}bg-success
|
{% if job.status == "ACTIVE" %}bg-success
|
||||||
{% elif job.status == "DRAFT" %}bg-secondary
|
{% elif job.status == "DRAFT" %}bg-secondary
|
||||||
{% elif job.status == "CLOSED" %}bg-warning
|
{% elif job.status == "CLOSED" %}bg-warning
|
||||||
{% elif job.status == "CANCELLED" %}bg-danger
|
{% elif job.status == "CANCELLED" %}bg-danger
|
||||||
{% elif job.status == "ARCHIVED" %}bg-secondary
|
{% elif job.status == "ARCHIVED" %}bg-secondary
|
||||||
{% else %}bg-secondary{% endif %}">
|
{% else %}bg-secondary{% endif %}'>
|
||||||
{{ job.get_status_display }}
|
{{ job.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -219,6 +219,11 @@
|
|||||||
|
|
||||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
|
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
|
||||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
|
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
|
||||||
|
<div class="float-end">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"N/A" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
|
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -303,7 +308,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item flex-fill" role="presentation">
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
<button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false">
|
<button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false">
|
||||||
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Staff" %}
|
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Assigned Staff" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item flex-fill" role="presentation">
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
@ -363,20 +368,16 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% if job.form_template.is_active %}
|
{% if job.form_template.is_active %}
|
||||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
||||||
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
|
<i class="fas fa-eye me-1"></i> {% trans "View" %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
||||||
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Form Template" %}
|
<i class="fas fa-list-alt me-1"></i> {% trans "Manage" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -384,42 +385,47 @@
|
|||||||
<div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab">
|
<div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab">
|
||||||
<h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5>
|
<h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5>
|
||||||
<div class="d-grid gap-3">
|
<div class="d-grid gap-3">
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
{% trans "Assign staff members to manage this job posting and track applications." %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a href="{% url 'staff_assignment_view' job.slug %}" class="btn btn-main-action">
|
|
||||||
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% if job.assigned_to %}
|
{% if job.assigned_to %}
|
||||||
<div class="mt-3">
|
<p class="text-muted small mb-3">
|
||||||
<h6 class="text-muted">{% trans "Current Assignments" %}</h6>
|
<strong>{% trans "Assigned to:" %}</strong> {{ job.assigned_to }}
|
||||||
{% for assignment in job.staff_assignments.all %}
|
</p>
|
||||||
<div class="card mb-2">
|
{% endif %}
|
||||||
<div class="card-body p-2">
|
{% if not job.assigned_to %}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
|
||||||
<div>
|
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
||||||
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
|
</button>
|
||||||
<br>
|
{% elif job.assigned_to and job.assigned_to == request.user %}
|
||||||
<small class="text-muted">{{ assignment.staff.email }}</small>
|
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
|
||||||
</div>
|
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
||||||
<div>
|
</button>
|
||||||
{% if assignment.staff.is_active %}
|
{% endif %}
|
||||||
<span class="badge bg-success">Active</span>
|
|
||||||
{% else %}
|
<!-- Modal for Staff Assignment -->
|
||||||
<span class="badge bg-danger">Inactive</span>
|
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
|
||||||
{% endif %}
|
<div class="modal-dialog">
|
||||||
</div>
|
<div class="modal-content">
|
||||||
</div>
|
<div class="modal-header">
|
||||||
{% if assignment.notes %}
|
<h5 class="modal-title" id="staffAssignmentModalLabel">
|
||||||
<small class="text-muted d-block mt-1">{{ assignment.notes }}</small>
|
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
|
||||||
{% endif %}
|
</h5>
|
||||||
</div>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="modal-body">
|
||||||
|
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{staff_form|crispy}}
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
<button type="submit" class="btn btn-main-action">
|
||||||
|
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
|
|
||||||
|
{% if not job.assigned_to %}
|
||||||
<div class="alert alert-info p-2 small mb-0">
|
<div class="alert alert-info p-2 small mb-0">
|
||||||
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}
|
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
--kaauh-border: #eaeff3;
|
--kaauh-border: #eaeff3;
|
||||||
--kaauh-primary-text: #343a40;
|
--kaauh-primary-text: #343a40;
|
||||||
--kaauh-gray-light: #f8f9fa;
|
--kaauh-gray-light: #f8f9fa;
|
||||||
--kaauh-warning: #ffc107;
|
--kaauh-warning: #ffc107;
|
||||||
--kaauh-danger: #dc3545;
|
--kaauh-danger: #dc3545;
|
||||||
--kaauh-success: #28a745;
|
--kaauh-success: #28a745;
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@
|
|||||||
<option value="Onsite" {% if type_filter == 'Onsite' %}selected{% endif %}>{% trans "Onsite" %}</option>
|
<option value="Onsite" {% if type_filter == 'Onsite' %}selected{% endif %}>{% trans "Onsite" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
|
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
|
||||||
<select name="status" id="status" class="form-select form-select-sm">
|
<select name="status" id="status" class="form-select form-select-sm">
|
||||||
@ -190,7 +190,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if meetings_data %}
|
{% if meetings_data %}
|
||||||
<div id="meetings-list">
|
<div id="meetings-list">
|
||||||
{# View Switcher (not provided, assuming standard include) #}
|
{# View Switcher (not provided, assuming standard include) #}
|
||||||
@ -213,7 +213,7 @@
|
|||||||
<p class="card-text text-muted small mb-3">
|
<p class="card-text text-muted small mb-3">
|
||||||
<i class="fas fa-user"></i> {% trans "Candidate" %}: {{ meeting.interview.application.person.full_name|default:"N/A" }}<br>
|
<i class="fas fa-user"></i> {% trans "Candidate" %}: {{ meeting.interview.application.person.full_name|default:"N/A" }}<br>
|
||||||
<i class="fas fa-briefcase"></i> {% trans "Job" %}: {{ meeting.interview.job.title|default:"N/A" }}<br>
|
<i class="fas fa-briefcase"></i> {% trans "Job" %}: {{ meeting.interview.job.title|default:"N/A" }}<br>
|
||||||
|
|
||||||
{# Dynamic location/type details #}
|
{# Dynamic location/type details #}
|
||||||
{% if meeting.type == 'Remote' %}
|
{% if meeting.type == 'Remote' %}
|
||||||
<i class="fas fa-link"></i> {% trans "Remote ID" %}: {{ meeting.meeting_id|default:meeting.location.id }}<br>
|
<i class="fas fa-link"></i> {% trans "Remote ID" %}: {{ meeting.meeting_id|default:meeting.location.id }}<br>
|
||||||
@ -224,7 +224,7 @@
|
|||||||
<i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br>
|
<i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br>
|
||||||
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes
|
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span class="status-badge bg-{{ meeting.status }}">
|
<span class="status-badge bg-{{ meeting.status }}">
|
||||||
{{ meeting.interview.get_status_display }}
|
{{ meeting.interview.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
@ -244,7 +244,7 @@
|
|||||||
<i class="fas fa-check"></i> {% trans "Physical Event" %}
|
<i class="fas fa-check"></i> {% trans "Physical Event" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# CORRECTED: Passing the slug to the update URL #}
|
{# CORRECTED: Passing the slug to the update URL #}
|
||||||
<a href="" class="btn btn-sm btn-outline-secondary">
|
<a href="" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
@ -287,7 +287,7 @@
|
|||||||
<td><strong class="text-primary"><a href="" class="text-decoration-none text-secondary">{{ meeting.topic }}<a></strong></td>
|
<td><strong class="text-primary"><a href="" class="text-decoration-none text-secondary">{{ meeting.topic }}<a></strong></td>
|
||||||
<td>
|
<td>
|
||||||
{# Display the event type badge #}
|
{# Display the event type badge #}
|
||||||
<span class="status-badge bg-{{ meeting.type }}">{{ meeting.type|title }}</span>
|
<span class="status-badge bg-primary-theme text-white">{{ meeting.type|title }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.application.person.slug %}">{{ meeting.interview.application.person.full_name }} <i class="fas fa-link"></i></a>
|
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.application.person.slug %}">{{ meeting.interview.application.person.full_name }} <i class="fas fa-link"></i></a>
|
||||||
@ -299,7 +299,7 @@
|
|||||||
<td>{{ meeting.duration }} min</td>
|
<td>{{ meeting.duration }} min</td>
|
||||||
<td>
|
<td>
|
||||||
{# Display the meeting status badge from the ScheduledInterview model #}
|
{# Display the meeting status badge from the ScheduledInterview model #}
|
||||||
<span class="status-badge bg-{{ meeting.status }}">
|
<span class="status-badge bg-primary-theme text-white">
|
||||||
{{ meeting.interview.get_status_display }}
|
{{ meeting.interview.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
{# ========================================================= #}
|
{# ========================================================= #}
|
||||||
@ -360,7 +377,7 @@ body {
|
|||||||
{# ========================================================= #}
|
{# ========================================================= #}
|
||||||
<div class="row g-4 mt-1">
|
<div class="row g-4 mt-1">
|
||||||
|
|
||||||
<div class="col-lg-12">
|
{% comment %} <div class="col-lg-12">
|
||||||
<div class="card" 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">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
||||||
@ -369,7 +386,6 @@ body {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body overflow-auto">
|
<div class="card-body overflow-auto">
|
||||||
|
|
||||||
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
|
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
|
||||||
<div id="comment-section" class="mb-4">
|
<div id="comment-section" class="mb-4">
|
||||||
{% if meeting.comments.all %}
|
{% if meeting.comments.all %}
|
||||||
@ -454,7 +470,7 @@ body {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -470,16 +486,9 @@ body {
|
|||||||
|
|
||||||
<form method="post" action="{% url 'create_interview_participants' meeting.interview.slug %}">
|
<form method="post" action="{% url 'create_interview_participants' meeting.interview.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
||||||
<div class="modal-body table-responsive">
|
<div class="modal-body table-responsive">
|
||||||
|
|
||||||
{{ meeting.name }}
|
{{ meeting.name }}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table class="table tab table-bordered mt-3">
|
<table class="table tab table-bordered mt-3">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="col">👥 {% trans "Participants" %}</th>
|
<th class="col">👥 {% trans "Participants" %}</th>
|
||||||
@ -497,10 +506,7 @@ body {
|
|||||||
{{ form.system_users }}
|
{{ form.system_users }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@ -228,3 +228,4 @@ setInterval(() => {
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -252,147 +252,15 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" id="person-form">
|
<form method="post" action="{% url 'person_update' person.slug %}" enctype="multipart/form-data" id="person-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{form|crispy}}
|
||||||
<!-- Profile Image Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
|
|
||||||
<div id="image-preview-container">
|
|
||||||
{% if person.profile_image %}
|
|
||||||
<img src="{{ person.profile_image.url }}" alt="Current Profile"
|
|
||||||
class="profile-image-preview">
|
|
||||||
<h5 class="text-muted mt-3">{% trans "Click to change photo" %}</h5>
|
|
||||||
<p class="text-muted small">{% trans "Current photo will be replaced" %}</p>
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-camera fa-3x text-muted mb-3"></i>
|
|
||||||
<h5 class="text-muted">{% trans "Upload Profile Photo" %}</h5>
|
|
||||||
<p class="text-muted small">{% trans "Click to browse or drag and drop" %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<input type="file" name="profile_image" id="id_profile_image"
|
|
||||||
class="d-none" accept="image/*">
|
|
||||||
</div>
|
|
||||||
{% if person.profile_image %}
|
|
||||||
<div class="mt-2">
|
|
||||||
<small class="text-muted">
|
|
||||||
{% trans "Leave empty to keep current photo" %}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Personal Information Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
|
|
||||||
<i class="fas fa-user me-2"></i> {% trans "Personal Information" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.first_name|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.middle_name|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.last_name|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Information Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
|
|
||||||
<i class="fas fa-envelope me-2"></i> {% trans "Contact Information" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
{{ form.email|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
{{ form.phone|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Information Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
|
|
||||||
<i class="fas fa-info-circle me-2"></i> {% trans "Additional Information" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.date_of_birth|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.nationality|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.gender|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
|
|
||||||
<i class="fas fa-map-marker-alt me-2"></i> {% trans "Address Information" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
{{ form.address|as_crispy_field }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LinkedIn Profile Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
|
|
||||||
<i class="fab fa-linkedin me-2"></i> {% trans "Professional Profile" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label for="id_linkedin_profile" class="form-label">
|
|
||||||
{% trans "LinkedIn Profile URL" %}
|
|
||||||
</label>
|
|
||||||
<input type="url" name="linkedin_profile" id="id_linkedin_profile"
|
|
||||||
class="form-control" placeholder="https://linkedin.com/in/username"
|
|
||||||
value="{{ person.linkedin_profile|default:'' }}">
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
{% trans "Optional: Add LinkedIn profile URL" %}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Form Actions -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<a href="{% url 'person_detail' person.slug %}" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-list me-1"></i> {% trans "Back to List" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="reset" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-undo me-1"></i> {% trans "Reset Changes" %}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-main-action">
|
|
||||||
<i class="fas fa-save me-1"></i> {% trans "Update Applicant" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button form="person-form" type="submit" class="btn btn-main-action">
|
||||||
|
<i class="fas fa-save me-1"></i> {% trans "Update" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -73,8 +73,25 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="agencyNavbar">
|
<div class="collapse navbar-collapse" id="agencyNavbar">
|
||||||
<div class="navbar-nav ms-auto">
|
|
||||||
|
|
||||||
|
<div class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item me-2">
|
||||||
|
{% if LANGUAGE_CODE == 'en' %}
|
||||||
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
|
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
||||||
|
<span class="me-2">🇸🇦</span> العربية
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% elif LANGUAGE_CODE == 'ar' %}
|
||||||
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
|
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
||||||
|
<span class="me-2">🇺🇸</span> English
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{# NAVIGATION LINKS (Add your portal links here if needed) #}
|
{# NAVIGATION LINKS (Add your portal links here if needed) #}
|
||||||
{% if request.user.user_type == 'agency' %}
|
{% if request.user.user_type == 'agency' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
@ -100,7 +117,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
|
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
|
||||||
@ -113,7 +130,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li class="nav-item dropdown">
|
{% comment %} <li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
|
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
|
||||||
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
||||||
<i class="fas fa-globe me-1"></i>
|
<i class="fas fa-globe me-1"></i>
|
||||||
@ -138,6 +155,16 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
|
||||||
|
<i class="fas fa-user-circle me-1"></i> <span>{% trans "My Profile" %}</span></a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item me-2">
|
||||||
|
<a class="nav-link text-white" href="{% url 'message_list' %}">
|
||||||
|
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item ms-3">
|
<li class="nav-item ms-3">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
@ -250,6 +277,21 @@
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function closeOpenBootstrapModal() {
|
||||||
|
const openModalElement = document.querySelector('.modal.show');
|
||||||
|
if (openModalElement) {
|
||||||
|
const modal = bootstrap.Modal.getInstance(openModalElement);
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
} else {
|
||||||
|
console.warn("Found an open modal element, but could not get the Bootstrap Modal instance.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No open Bootstrap Modal found to close.");
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -238,11 +238,13 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Name" %}</th>
|
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate"%}</th>
|
||||||
<th>{% trans "Contact" %}</th>
|
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
|
||||||
<th>{% trans "Stage" %}</th>
|
</th>
|
||||||
<th>{% trans "Submitted" %}</th>
|
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
|
||||||
<th>{% trans "Actions" %}</th>
|
</th>
|
||||||
|
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Submitted"%}</th>
|
||||||
|
<th class="px-4 py-3 text-uppercase small fw-bold text-muted text-end">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -262,10 +264,19 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
{{ candidate.created_at|date:"Y-m-d H:i" }}
|
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>
|
||||||
|
{{candidate.email }}</div>
|
||||||
|
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-4">
|
||||||
|
<span class="badge bg-soft-info text-info rounded-pill px-3">
|
||||||
|
{{candidate.get_stage_display }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4">
|
||||||
|
<span class="small text-muted">{{ candidate.created_at|date:"M d, Y" }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 text-end">
|
||||||
<a href="{% url 'candidate_detail' candidate.slug %}"
|
<a href="{% url 'candidate_detail' candidate.slug %}"
|
||||||
class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
|
class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
@ -315,9 +326,9 @@
|
|||||||
cy="60"
|
cy="60"
|
||||||
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
|
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="progress-ring-text">
|
<div class="position-absolute top-50 start-50 translate-middle text-center">
|
||||||
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
|
<div class="h3 fw-bold mb-0 text-dark">{{ total_candidates }}</div>
|
||||||
{{ progress|floatformat:0 }}%
|
<div class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -146,7 +146,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="text-muted small">{% trans "Status" %}</label>
|
<label class="text-muted small">{% trans "Status" %}</label>
|
||||||
<div>
|
<div>
|
||||||
<span class="status-badge status-{{ assignment.status }}">
|
<span class="status-badge status-{{ assignment.status }}" >
|
||||||
{{ assignment.get_status_display }}
|
{{ assignment.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -264,12 +264,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="editCandidate({{ candidate.id }})" title="{% trans 'Edit Candidate' %}">
|
<a href="{% url 'candidate_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</a>
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteCandidate({{ candidate.id }}, '{{ candidate.name }}')" title="{% trans 'Remove Candidate' %}">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@ -147,7 +147,7 @@
|
|||||||
<div class="kaauh-card shadow-sm">
|
<div class="kaauh-card shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive person-table">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
@ -155,8 +155,8 @@
|
|||||||
<th scope="col">{% trans "Email" %}</th>
|
<th scope="col">{% trans "Email" %}</th>
|
||||||
<th scope="col">{% trans "Phone" %}</th>
|
<th scope="col">{% trans "Phone" %}</th>
|
||||||
<th scope="col">{% trans "Job" %}</th>
|
<th scope="col">{% trans "Job" %}</th>
|
||||||
<th scope="col">{% trans "Stage" %}</th>
|
{% comment %} <th scope="col">{% trans "Stage" %}</th>
|
||||||
<th scope="col">{% trans "Applied Date" %}</th>
|
<th scope="col">{% trans "Applied Date" %}</th> {% endcomment %}
|
||||||
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -183,7 +183,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ person.phone|default:"-" }}</td>
|
<td>{{ person.phone|default:"-" }}</td>
|
||||||
<td>
|
{% comment %} <td>
|
||||||
<span class="badge bg-light text-dark">
|
<span class="badge bg-light text-dark">
|
||||||
{{ person.job.title|truncatechars:30 }}
|
{{ person.job.title|truncatechars:30 }}
|
||||||
</span>
|
</span>
|
||||||
@ -201,19 +201,19 @@
|
|||||||
{{ person.get_stage_display }}
|
{{ person.get_stage_display }}
|
||||||
</span>
|
</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td> {% endcomment %}
|
||||||
<td>{{ person.created_at|date:"Y-m-d" }}</td>
|
<td>{{ person.created_at|date:"Y-m-d" }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{% url 'candidate_detail' person.slug %}"
|
<button type="button" data-bs-toggle="modal" data-bs-target="#updateModal"
|
||||||
class="btn btn-sm btn-outline-primary"
|
hx-get="{% url 'person_update' person.slug %}"
|
||||||
title="{% trans 'View Details' %}">
|
hx-target="#updateModalBody"
|
||||||
<i class="fas fa-eye"></i>
|
hx-swap="outerrHTML"
|
||||||
</a>
|
hx-select="#person-form"
|
||||||
<button type="button"
|
hx-vals='{"view":"portal"}'
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
title="{% trans 'Edit Person' %}"
|
title="{% trans 'Edit Person' %}"
|
||||||
onclick="editPerson({{ person.id }})">
|
>
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -290,6 +290,28 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty Modal -->
|
||||||
|
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content" hx-boost="true" hx-vals='{"view":"portal"}' hx-select=".person-table" hx-target=".person-table"
|
||||||
|
hx-swap="outerHTML" hx-on::after-request="closeOpenBootstrapModal()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="updateModalLabel">{% trans "Update" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="updateModalBody">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button form="person-form" type="submit" class="btn btn-main-action">
|
||||||
|
<i class="fas fa-save me-1"></i> {% trans "Update" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Person Modal -->
|
<!-- Person Modal -->
|
||||||
<div class="modal fade modal-lg" id="personModal" tabindex="-1" aria-labelledby="personModalLabel" aria-hidden="true">
|
<div class="modal fade modal-lg" id="personModal" tabindex="-1" aria-labelledby="personModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
/* Kaauh Theme Variables - Assuming these are defined in portal_base */
|
/* Kaauh Theme Variables - Assuming these are defined in portal_base */
|
||||||
:root {
|
:root {
|
||||||
/* Assuming these are carried from your global CSS/base template */
|
/* Assuming these are carried from your global CSS/base template */
|
||||||
--kaauh-teal: #00636e;
|
--kaauh-teal: #00636e;
|
||||||
--kaauh-teal-dark: #004a53;
|
--kaauh-teal-dark: #004a53;
|
||||||
--kaauh-border: #eaeff3;
|
--kaauh-border: #eaeff3;
|
||||||
--kaauh-primary-text: #343a40;
|
--kaauh-primary-text: #343a40;
|
||||||
@ -31,11 +31,11 @@
|
|||||||
.application-progress {
|
.application-progress {
|
||||||
position: relative;
|
position: relative;
|
||||||
/* Use flexbox for layout */
|
/* Use flexbox for layout */
|
||||||
display: flex;
|
display: flex;
|
||||||
/* Use gap for consistent space between elements */
|
/* Use gap for consistent space between elements */
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
/* Center the timeline content */
|
/* Center the timeline content */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 2rem 0 3rem; /* Extra spacing below timeline */
|
margin: 2rem 0 3rem; /* Extra spacing below timeline */
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
overflow-x: auto; /* Allow horizontal scroll for small screens */
|
overflow-x: auto; /* Allow horizontal scroll for small screens */
|
||||||
@ -46,9 +46,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* Prevent shrinking */
|
/* Prevent shrinking */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
/* Added min-width for label spacing */
|
/* Added min-width for label spacing */
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Timeline Connector Line */
|
/* Timeline Connector Line */
|
||||||
@ -57,7 +57,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
/* Adjust position to connect centered steps */
|
/* Adjust position to connect centered steps */
|
||||||
left: calc(-50% + 20px);
|
left: calc(-50% + 20px);
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--kaauh-border);
|
background: var(--kaauh-border);
|
||||||
@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
.progress-step.active::before {
|
.progress-step.active::before {
|
||||||
/* Line leading up to the active step should be completed/success color */
|
/* Line leading up to the active step should be completed/success color */
|
||||||
background: var(--kaauh-success);
|
background: var(--kaauh-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-icon {
|
.progress-icon {
|
||||||
@ -102,7 +102,7 @@
|
|||||||
background: var(--kaauh-teal);
|
background: var(--kaauh-teal);
|
||||||
color: white;
|
color: white;
|
||||||
/* Add a subtle shadow for focus */
|
/* Add a subtle shadow for focus */
|
||||||
box-shadow: 0 0 0 4px rgba(0, 99, 110, 0.2);
|
box-shadow: 0 0 0 4px rgba(0, 99, 110, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-label {
|
.progress-label {
|
||||||
@ -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,12 +197,13 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<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">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h4 class="mb-1">
|
<h4 class="mb-1">
|
||||||
@ -220,7 +223,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body pt-5">
|
<div class="card-body pt-5">
|
||||||
<div class="application-progress">
|
<div class="application-progress">
|
||||||
<div class="progress-step {% if application.stage != 'Applied' and application.stage != 'Rejected' %}completed{% else %}active{% endif %}" style="flex: 0 0 auto;">
|
<div class="progress-step {% if application.stage != 'Applied' and application.stage != 'Rejected' %}completed{% else %}active{% endif %}" style="flex: 0 0 auto;">
|
||||||
<div class="progress-icon">
|
<div class="progress-icon">
|
||||||
@ -275,9 +278,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-4 g-4">
|
<div class="row mt-4 g-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="text-center p-3 bg-light rounded h-100 shadow-sm">
|
<div class="text-center p-3 bg-light rounded h-100 shadow-sm">
|
||||||
<i class="fas fa-calendar-alt text-primary-theme fa-2x mb-2"></i>
|
<i class="fas fa-calendar-alt text-primary-theme fa-2x mb-2"></i>
|
||||||
<h6 class="text-muted small">{% trans "Applied Date" %}</h6>
|
<h6 class="text-muted small">{% trans "Applied Date" %}</h6>
|
||||||
<p class="mb-0 fw-bold">{{ application.created_at|date:"M d, Y" }}</p>
|
<p class="mb-0 fw-bold">{{ application.created_at|date:"M d, Y" }}</p>
|
||||||
@ -309,8 +312,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
<div class="col-md-6 col-6">
|
<div class="col-md-6 col-6">
|
||||||
<a href="{% url 'candidate_portal_dashboard' %}" class="text-decoration-none text-dark">
|
<a href="{% url 'candidate_portal_dashboard' %}" class="text-decoration-none text-dark">
|
||||||
<div class="kaauh-card h-50 shadow-sm action-card">
|
<div class="kaauh-card h-50 shadow-sm action-card">
|
||||||
@ -327,15 +330,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if application.resume %}
|
{% if application.resume %}
|
||||||
<div class="col-md-6 col-6">
|
<div class="col-md-6 col-6">
|
||||||
<a href="{{ application.resume.url }}"
|
<a href="{{ application.resume.url }}"
|
||||||
target="_blank" class="text-decoration-none text-dark">
|
target="_blank" class="text-decoration-none text-dark">
|
||||||
<div class="kaauh-card h-50 shadow-sm action-card">
|
<div class="kaauh-card h-50 shadow-sm action-card">
|
||||||
<div class="card-body text-center mb-4">
|
<div class="card-body text-center mb-4">
|
||||||
<i class="fas fa-file-download fa-2x text-primary-theme mb-3"></i>
|
<i class="fas fa-file-download fa-2x text-primary-theme mb-3"></i>
|
||||||
<h6>{% trans "Download Resume" %}</h6>
|
<h6>{% trans "Download Resume" %}</h6>
|
||||||
<p class="text-muted small">{% trans "Get your submitted file" %}</p>
|
<p class="text-muted small">{% trans "Get your submitted file" %}</p>
|
||||||
<a href="{{ application.resume.url }}"
|
<a href="{{ application.resume.url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-main-action btn-sm w-100">
|
class="btn btn-main-action btn-sm w-100">
|
||||||
<i class="fas fa-download me-2"></i>
|
<i class="fas fa-download me-2"></i>
|
||||||
@ -346,25 +349,25 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% if interviews %}
|
{% if interviews %}
|
||||||
<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" %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
{% if interviews %}
|
{% if interviews %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Date" %}</th>
|
<th>{% trans "Date" %}</th>
|
||||||
@ -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>
|
||||||
@ -436,7 +439,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if application.stage == "Document Review" %}
|
{% if application.stage == "Document Review" %}
|
||||||
<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-primary-theme text-white">
|
<div class="card-header bg-primary-theme text-white">
|
||||||
@ -455,7 +458,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
{% if documents %}
|
{% if documents %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
@ -475,7 +478,7 @@
|
|||||||
{% if document.file %}
|
{% if document.file %}
|
||||||
<a href="{{ document.file.url }}"
|
<a href="{{ document.file.url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-decoration-none text-primary-theme">
|
class="text-decoration-none text-primary-theme">
|
||||||
<i class="fas fa-file-pdf text-danger me-2"></i>
|
<i class="fas fa-file-pdf text-danger me-2"></i>
|
||||||
{{ document.get_document_type_display }}
|
{{ document.get_document_type_display }}
|
||||||
</a>
|
</a>
|
||||||
@ -533,6 +536,47 @@
|
|||||||
{% 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 %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Next Steps Section -->
|
||||||
|
<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">
|
||||||
@ -541,7 +585,7 @@
|
|||||||
{% trans "Next Steps" %}
|
{% trans "Next Steps" %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
{% if application.stage == 'Applied' %}
|
{% if application.stage == 'Applied' %}
|
||||||
<div class="alert bg-primary-theme text-white">
|
<div class="alert bg-primary-theme text-white">
|
||||||
<i class="fas fa-clock me-2"></i>
|
<i class="fas fa-clock me-2"></i>
|
||||||
|
|||||||
@ -206,7 +206,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||||
|
|
||||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||||
<option selected>
|
<option selected>
|
||||||
----------
|
----------
|
||||||
@ -233,7 +233,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-info btn-sm"
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -248,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||||
@ -291,7 +291,7 @@
|
|||||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||||
hx-target="#candidateviewModalBody"
|
hx-target="#candidateviewModalBody"
|
||||||
title="View Profile">
|
title="View Profile">
|
||||||
{{ candidate.name }}<i class="fas fa-eye ms-1"></i>
|
{{ candidate.name }}
|
||||||
</button>
|
</button>
|
||||||
{% comment %} <div class="candidate-name">
|
{% comment %} <div class="candidate-name">
|
||||||
{{ candidate.name }}
|
{{ candidate.name }}
|
||||||
@ -380,7 +380,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
{% if candidate.get_latest_meeting %}
|
{% if candidate.get_latest_meeting %}
|
||||||
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
||||||
|
|
||||||
@ -402,7 +402,7 @@
|
|||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else%}
|
{% else%}
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#candidateviewModal"
|
data-bs-target="#candidateviewModal"
|
||||||
@ -422,7 +422,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-main-action btn-sm"
|
<button type="button" class="btn btn-main-action btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -497,7 +497,7 @@
|
|||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||||
{% trans "Loading email form..." %}
|
{% trans "Loading email form..." %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static i18n %}
|
||||||
|
|
||||||
{% block title %}{{ source.name }} - Source Details{% endblock %}
|
{% block title %}{{ source.name }} - {% trans "Source Details" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<h1 class="h3 mb-0">{{ source.name }}</h1>
|
<h1 class="h3 mb-0">{{ source.name }}</h1>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-edit"></i> Edit
|
<i class="fas fa-edit"></i> {% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
{% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
{% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||||
<i class="fas fa-key"></i> Generate Keys
|
<i class="fas fa-key"></i> Generate Keys
|
||||||
@ -24,13 +24,12 @@
|
|||||||
hx-select="#toggle-source-status"
|
hx-select="#toggle-source-status"
|
||||||
hx-select-oob="#source-status"
|
hx-select-oob="#source-status"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
|
hx-confirm="{% blocktrans %}Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?{% endblocktrans %}">
|
||||||
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
|
|
||||||
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||||
{{ source.is_active|yesno:'Deactivate,Activate' }}
|
{{ source.is_active|yesno:'Deactivate,Activate' }}
|
||||||
</button>
|
</button>
|
||||||
<a href="{% url 'source_delete' source.pk %}" class="btn btn-outline-danger">
|
<a href="{% url 'source_delete' source.pk %}" class="btn btn-outline-danger">
|
||||||
<i class="fas fa-trash"></i> Delete
|
<i class="fas fa-trash"></i> {% trans "Delete" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,19 +39,19 @@
|
|||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6 class="mb-0">Source Information</h6>
|
<h6 class="mb-0">{% trans "Source Information" %}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Name</label>
|
<label class="form-label text-muted">{% trans "Name" %}</label>
|
||||||
<div class="fw-bold">{{ source.name }}</div>
|
<div class="fw-bold">{{ source.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Type</label>
|
<label class="form-label text-muted">{% trans "Type" %}</label>
|
||||||
<div>
|
<div>
|
||||||
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -62,7 +61,7 @@
|
|||||||
|
|
||||||
{% if source.description %}
|
{% if source.description %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Description</label>
|
<label class="form-label text-muted">{{ _("Description") }}</label>
|
||||||
<div>{{ source.description|linebreaks }}</div>
|
<div>{{ source.description|linebreaks }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -70,24 +69,24 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Contact Email</label>
|
<label class="form-label text-muted">{{ _("Contact Email") }}</label>
|
||||||
<div>
|
<div>
|
||||||
{% if source.contact_email %}
|
{% if source.contact_email %}
|
||||||
<a href="mailto:{{ source.contact_email }}">{{ source.contact_email }}</a>
|
<a href="mailto:{{ source.contact_email }}">{{ source.contact_email }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not specified</span>
|
<span class="text-muted">{{ _("Not specified") }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Contact Phone</label>
|
<label class="form-label text-muted">{{ _("Contact Phone") }}</label>
|
||||||
<div>
|
<div>
|
||||||
{% if source.contact_phone %}
|
{% if source.contact_phone %}
|
||||||
{{ source.contact_phone }}
|
{{ source.contact_phone }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not specified</span>
|
<span class="text-muted">{{ _("Not specified") }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -97,24 +96,24 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Status</label>
|
<label class="form-label text-muted">{{ _("Status") }}</label>
|
||||||
<div id="source-status">
|
<div id="source-status">
|
||||||
{% if source.is_active %}
|
{% if source.is_active %}
|
||||||
<span class="badge bg-success">Active</span>
|
<span class="badge bg-success">{{ _("Active") }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Inactive</span>
|
<span class="badge bg-secondary">{{ _("Inactive") }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Requires Authentication</label>
|
<label class="form-label text-muted">{% trans "Requires Authentication" %}</label>
|
||||||
<div>
|
<div>
|
||||||
{% if source.requires_auth %}
|
{% if source.requires_auth %}
|
||||||
<span class="badge bg-warning">Yes</span>
|
<span class="badge bg-warning">{% trans "Yes" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">No</span>
|
<span class="badge bg-secondary">{% trans "No" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -123,21 +122,21 @@
|
|||||||
|
|
||||||
{% if source.webhook_url %}
|
{% if source.webhook_url %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Webhook URL</label>
|
<label class="form-label text-muted">{% trans "Webhook URL" %}</label>
|
||||||
<div><code>{{ source.webhook_url }}</code></div>
|
<div><code>{{ source.webhook_url }}</code></div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if source.api_timeout %}
|
{% if source.api_timeout %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">API Timeout</label>
|
<label class="form-label text-muted">{{ _("API Timeout") }}</label>
|
||||||
<div>{{ source.api_timeout }} seconds</div>
|
<div>{{ source.api_timeout }} {{ _("seconds") }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if source.notes %}
|
{% if source.notes %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Notes</label>
|
<label class="form-label text-muted">{{ _("Notes") }}</label>
|
||||||
<div>{{ source.notes|linebreaks }}</div>
|
<div>{{ source.notes|linebreaks }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -145,13 +144,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Created</label>
|
<label class="form-label text-muted">{{ _("Created") }}</label>
|
||||||
<div>{{ source.created_at|date:"M d, Y H:i" }}</div>
|
<div>{{ source.created_at|date:"M d, Y H:i" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Last Updated</label>
|
<label class="form-label text-muted">{{ _("Last Updated") }}</label>
|
||||||
<div>{{ source.updated_at|date:"M d, Y H:i" }}</div>
|
<div>{{ source.updated_at|date:"M d, Y H:i" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,23 +163,23 @@
|
|||||||
<!-- API Credentials -->
|
<!-- API Credentials -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6 class="mb-0">API Credentials</h6>
|
<h6 class="mb-0">{% trans "API Credentials" %}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div id="api-credentials" class="card-body">
|
<div id="api-credentials" class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">API Key</label>
|
<label class="form-label text-muted">{% trans "API Key" %}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
hx-post="{% url 'copy_to_clipboard' %}"
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
hx-vals='{"text": "{{ source.api_key }}"}'
|
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||||
title="Copy to clipboard">
|
title="{% trans "Copy to clipboard" %}">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">API Secret</label>
|
<label class="form-label text-muted">{% trans "API Secret" %}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||||
@ -189,14 +188,14 @@
|
|||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
hx-post="{% url 'copy_to_clipboard' %}"
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
hx-vals='{"text": "{{ source.api_secret }}"}'
|
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||||
title="Copy to clipboard">
|
title="{% trans "Copy to clipboard" %}">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<a hx-post="{% url 'generate_api_keys' source.pk %}" hx-target="#api-credentials" hx-select="#api-credentials" hx-swap="outerHTML" class="btn btn-main-action btn-sm">
|
<a hx-post="{% url 'generate_api_keys' source.pk %}" hx-target="#api-credentials" hx-select="#api-credentials" hx-swap="outerHTML" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-key"></i> Generate New Keys
|
<i class="fas fa-key"></i> {% trans "Generate New Keys" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -205,24 +204,24 @@
|
|||||||
<!-- Statistics -->
|
<!-- Statistics -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6 class="mb-0">Integration Statistics</h6>
|
<h6 class="mb-0">{% trans "Integration Statistics" %}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Total API Calls</label>
|
<label class="form-label text-muted">{% trans "Total API Calls" %}</label>
|
||||||
<div class="h5 mb-0">{{ total_logs }}</div>
|
<div class="h5 mb-0">{{ total_logs }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Successful Calls</label>
|
<label class="form-label text-muted">{% trans "Successful Calls" %}</label>
|
||||||
<div class="h5 mb-0 text-success">{{ successful_logs }}</div>
|
<div class="h5 mb-0 text-success">{{ successful_logs }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Failed Calls</label>
|
<label class="form-label text-muted">{% trans "Failed Calls" %}</label>
|
||||||
<div class="h5 mb-0 text-danger">{{ failed_logs }}</div>
|
<div class="h5 mb-0 text-danger">{{ failed_logs }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if total_logs > 0 %}
|
{% if total_logs > 0 %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">Success Rate</label>
|
<label class="form-label text-muted">{% trans "Success Rate" %}</label>
|
||||||
<div class="h5 mb-0">
|
<div class="h5 mb-0">
|
||||||
{% widthratio successful_logs total_logs 100 %}%
|
{% widthratio successful_logs total_logs 100 %}%
|
||||||
</div>
|
</div>
|
||||||
@ -236,8 +235,8 @@
|
|||||||
<!-- Integration Logs -->
|
<!-- Integration Logs -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">Recent Integration Logs</h6>
|
<h6 class="mb-0">{% trans "Recent Integration Logs" %}</h6>
|
||||||
<small class="text-muted">Last 10 logs</small>
|
<small class="text-muted">{% trans "Last 10 logs" %}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if integration_logs %}
|
{% if integration_logs %}
|
||||||
@ -245,11 +244,11 @@
|
|||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Timestamp</th>
|
<th>{% trans "Timestamp" %}</th>
|
||||||
<th>Method</th>
|
<th>{% trans "Method" %}</th>
|
||||||
<th>Status</th>
|
<th>{% trans "Status" %}</th>
|
||||||
<th>Response Time</th>
|
<th>{% trans "Response Time" %}</th>
|
||||||
<th>Details</th>
|
<th>{% trans "Details" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -286,7 +285,7 @@
|
|||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">No data</span>
|
<span class="text-muted">{{ _("No data") }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -297,7 +296,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
|
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
|
||||||
<p class="text-muted">No integration logs found</p>
|
<p class="text-muted">{{ _("No integration logs found") }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -313,24 +312,24 @@
|
|||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Integration Log Details</h5>
|
<h5 class="modal-title">{% trans "Integration Log Details" %}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<strong>Timestamp:</strong><br>
|
<strong>{{ _("Timestamp:") }}:</strong><br>
|
||||||
{{ log.created_at|date:"M d, Y H:i:s" }}
|
{{ log.created_at|date:"M d, Y H:i:s" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<strong>Method:</strong><br>
|
<strong>{{ _("Method:") }}:</strong><br>
|
||||||
<span class="badge bg-secondary">{{ log.method }}</span>
|
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<strong>Status Code:</strong><br>
|
<strong>{{ _("Status Code:") }}:</strong><br>
|
||||||
{% if log.status_code >= 200 and log.status_code < 300 %}
|
{% if log.status_code >= 200 and log.status_code < 300 %}
|
||||||
<span class="badge bg-success">{{ log.status_code }}</span>
|
<span class="badge bg-success">{{ log.status_code }}</span>
|
||||||
{% elif log.status_code >= 400 %}
|
{% elif log.status_code >= 400 %}
|
||||||
@ -340,7 +339,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<strong>Response Time:</strong><br>
|
<strong>{{ _("Response Time:") }}:</strong><br>
|
||||||
{% if log.response_time_ms %}
|
{% if log.response_time_ms %}
|
||||||
{{ log.response_time_ms }}ms
|
{{ log.response_time_ms }}ms
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -350,25 +349,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<strong>Request Data:</strong>
|
<strong>{{ _("Request Data:") }}:</strong>
|
||||||
<pre class="bg-light p-2 rounded"><code>{{ log.request_data|pprint }}</code></pre>
|
<pre class="bg-light p-2 rounded"><code>{{ log.request_data|pprint }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
{% if log.response_data %}
|
{% if log.response_data %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<strong>Response Data:</strong>
|
<strong>{{ _("Response Data:") }}:</strong>
|
||||||
<pre class="bg-light p-2 rounded"><code>{{ log.response_data|pprint }}</code></pre>
|
<pre class="bg-light p-2 rounded"><code>{{ log.response_data|pprint }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if log.error_message %}
|
{% if log.error_message %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<strong>Error Message:</strong>
|
<strong>{{ _("Error Message:") }}:</strong>
|
||||||
<div class="alert alert-danger">{{ log.error_message }}</div>
|
<div class="alert alert-danger">{{ log.error_message }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user