Compare commits
No commits in common. "99f62c21fada2097023b0a6d1f7a8b2194d41fa7" and "e87700a2138a1054cc293589f3d1aad5a1b92b74" have entirely different histories.
99f62c21fa
...
e87700a213
188
base.po
188
base.po
@ -1,188 +0,0 @@
|
|||||||
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
37
demo.po
@ -1,37 +0,0 @@
|
|||||||
#
|
|
||||||
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
167
demo1.po
@ -1,167 +0,0 @@
|
|||||||
#
|
|
||||||
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
10252
django1.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
9035
locale/ar/LC_MESSAGES/django.po.backup
Normal file
9035
locale/ar/LC_MESSAGES/django.po.backup
Normal file
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,34 +934,38 @@ 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"}
|
attrs={"class": "form-control", "placeholder": "agency@example.com"}
|
||||||
),
|
),
|
||||||
"phone": forms.TextInput(
|
"phone": forms.TextInput(
|
||||||
attrs={"class": "form-control"}
|
attrs={"class": "form-control", "placeholder": "+966 50 123 4567"}
|
||||||
),
|
),
|
||||||
"website": forms.URLInput(
|
"website": forms.URLInput(
|
||||||
attrs={"class": "form-control"}
|
attrs={"class": "form-control", "placeholder": "https://www.agency.com"}
|
||||||
),
|
),
|
||||||
"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",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -2486,11 +2490,12 @@ 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',is_superuser=False
|
user_type='staff'
|
||||||
).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'
|
||||||
@ -2511,4 +2516,3 @@ 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
|
||||||
|
|
||||||
|
|||||||
@ -1,159 +0,0 @@
|
|||||||
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)
|
application_deadline = models.DateField(db_index=True) # Added index
|
||||||
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,18 +1310,7 @@ 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):
|
||||||
@ -1331,7 +1320,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,
|
||||||
@ -1340,7 +1329,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,
|
||||||
@ -1348,16 +1337,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")
|
||||||
@ -1365,7 +1354,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,11 +442,6 @@ 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,
|
||||||
@ -665,9 +660,5 @@ 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,17 +212,12 @@ class PersonDetailView(DetailView):
|
|||||||
context_object_name = "person"
|
context_object_name = "person"
|
||||||
|
|
||||||
|
|
||||||
class PersonUpdateView( UpdateView):
|
class PersonUpdateView(StaffRequiredMixin, 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
|
||||||
@ -620,7 +615,6 @@ 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)
|
||||||
|
|
||||||
@ -631,7 +625,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()
|
||||||
|
|
||||||
@ -687,7 +681,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
|
||||||
@ -707,7 +701,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):
|
||||||
@ -1969,6 +1963,8 @@ 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,
|
||||||
@ -1978,6 +1974,8 @@ 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):
|
||||||
"""
|
"""
|
||||||
@ -2993,11 +2991,10 @@ 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():
|
||||||
job.assigned_to = form.cleaned_data["assigned_to"]
|
assignment = form.save()
|
||||||
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:
|
||||||
@ -3092,8 +3089,7 @@ 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(InterviewNote, slug=slug)
|
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
|
||||||
print(meeting)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = InterviewNoteForm(request.POST)
|
form = InterviewNoteForm(request.POST)
|
||||||
@ -3293,8 +3289,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)
|
||||||
|
|
||||||
@ -3351,7 +3347,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)
|
||||||
|
|
||||||
@ -4053,8 +4049,7 @@ def portal_login(request):
|
|||||||
return render(request, "recruitment/portal_login.html", context)
|
return render(request, "recruitment/portal_login.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@candidate_user_required
|
|
||||||
def candidate_portal_dashboard(request):
|
def candidate_portal_dashboard(request):
|
||||||
"""Candidate portal dashboard"""
|
"""Candidate portal dashboard"""
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
@ -4099,16 +4094,11 @@ def candidate_application_detail(request, slug):
|
|||||||
return redirect("account_login")
|
return redirect("account_login")
|
||||||
|
|
||||||
# Get candidate profile (Person record)
|
# Get candidate profile (Person record)
|
||||||
agency = getattr(request.user,"agency_profile",None)
|
try:
|
||||||
if agency:
|
candidate = request.user.person_profile
|
||||||
candidate = get_object_or_404(Application,slug=slug)
|
except:
|
||||||
# if Application.objects.filter(person=candidate,hirin).exists()
|
messages.error(request, "No candidate profile found.")
|
||||||
else:
|
return redirect("account_login")
|
||||||
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(
|
||||||
@ -4118,7 +4108,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 if agency else candidate
|
person=candidate
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get AI analysis data if available
|
# Get AI analysis data if available
|
||||||
@ -4684,11 +4674,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,
|
||||||
@ -4701,23 +4691,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 = {
|
||||||
@ -4767,7 +4757,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:
|
||||||
@ -4997,7 +4987,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", application_slug=application.slug)
|
return redirect("candidate_application_detail", 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":
|
||||||
@ -5278,7 +5268,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:
|
||||||
@ -5670,7 +5660,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"]
|
||||||
@ -5744,11 +5734,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)
|
||||||
|
|
||||||
|
|
||||||
@ -5771,7 +5761,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.
|
||||||
@ -5786,7 +5776,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")
|
||||||
@ -5798,11 +5788,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)
|
||||||
@ -5884,8 +5874,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.
|
||||||
@ -5896,7 +5886,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)."""
|
||||||
@ -6116,152 +6106,3 @@ 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,246 +1,44 @@
|
|||||||
import os
|
import requests
|
||||||
import json
|
import jwt
|
||||||
import time
|
import time
|
||||||
import argparse
|
|
||||||
import polib
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
from openai import OpenAI, APIConnectionError, RateLimitError
|
|
||||||
|
|
||||||
# --- Terminal Colors ---
|
ZOOM_API_KEY = 'OoDRW3uVTymEnnQWTZTsLQ'
|
||||||
class Colors:
|
ZOOM_API_SECRET = 'ZJ0hCFMrwekG71jbR3Trvoor4tK3HAVP'
|
||||||
HEADER = '\033[95m'
|
|
||||||
BLUE = '\033[94m'
|
|
||||||
GREEN = '\033[92m'
|
|
||||||
WARNING = '\033[93m'
|
|
||||||
FAIL = '\033[91m'
|
|
||||||
ENDC = '\033[0m'
|
|
||||||
BOLD = '\033[1m'
|
|
||||||
|
|
||||||
def print_success(msg): print(f"{Colors.GREEN}{msg}{Colors.ENDC}")
|
def generate_zoom_jwt():
|
||||||
def print_warning(msg): print(f"{Colors.WARNING}{msg}{Colors.ENDC}")
|
payload = {
|
||||||
def print_error(msg): print(f"{Colors.FAIL}{msg}{Colors.ENDC}")
|
'iss': ZOOM_API_KEY,
|
||||||
def print_info(msg): print(f"{Colors.BLUE}{msg}{Colors.ENDC}")
|
'exp': time.time() + 3600
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, ZOOM_API_SECRET, algorithm='HS256')
|
||||||
|
return token
|
||||||
|
|
||||||
# --- Provider Configurations ---
|
def create_zoom_meeting(topic, start_time, duration, host_email):
|
||||||
|
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__":
|
||||||
parser = argparse.ArgumentParser(description="Translate .po files using AI Providers (Z.ai, Ollama, OpenAI)")
|
topic = "Your Meeting Topic"
|
||||||
|
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)
|
|
||||||
@ -35,9 +35,6 @@
|
|||||||
#1e3a47 100%
|
#1e3a47 100%
|
||||||
);
|
);
|
||||||
background-image: url("{% static 'image/vision.svg' %}");
|
background-image: url("{% static 'image/vision.svg' %}");
|
||||||
@media (max-width: 768px) {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 60px;
|
background-position: 60px;
|
||||||
background-size: 320px auto;
|
background-size: 320px auto;
|
||||||
@ -61,11 +58,11 @@
|
|||||||
|
|
||||||
.wizard-container {
|
.wizard-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 900px; /* Increased max-width slightly for content */
|
max-width: 800px; /* Increased max-width slightly for content */
|
||||||
background: white;
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* Allow height to be determined by content, constrained by max-height */
|
/* Allow height to be determined by content, constrained by max-height */
|
||||||
@ -476,7 +473,7 @@
|
|||||||
class="navbar navbar-expand-lg sticky-top"
|
class="navbar navbar-expand-lg sticky-top"
|
||||||
style="background-color: var(--kaauh-teal); z-index: 1030"
|
style="background-color: var(--kaauh-teal); z-index: 1030"
|
||||||
>
|
>
|
||||||
<span class="ms-2 text-white">{% trans "JOB ID" %}: {{job_id}}</span>
|
<span class="ms-2 text-white">JOB ID: {{job_id}}</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="page-content-wrapper">
|
<div class="page-content-wrapper">
|
||||||
@ -489,7 +486,7 @@
|
|||||||
<div class="logo">
|
<div class="logo">
|
||||||
<i class="fas fa-file-alt"></i>
|
<i class="fas fa-file-alt"></i>
|
||||||
<span id="formTitle"
|
<span id="formTitle"
|
||||||
>{% trans "Application Form" %}</span
|
>{% translate "Application Form" %}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-text" id="progressText">1 of 1</div>
|
<div class="progress-text" id="progressText">1 of 1</div>
|
||||||
@ -504,7 +501,7 @@
|
|||||||
style="display: none"
|
style="display: none"
|
||||||
>
|
>
|
||||||
<h3 class="mb-4">
|
<h3 class="mb-4">
|
||||||
{% trans "Review Your Application" %}
|
{% translate "Review Your Application" %}
|
||||||
</h3>
|
</h3>
|
||||||
<div id="previewContent"></div>
|
<div id="previewContent"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -516,11 +513,11 @@
|
|||||||
class="nav-btn btn-back"
|
class="nav-btn btn-back"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
>
|
>
|
||||||
<i class="fas fa-arrow-left"></i> {% trans "Back" %}
|
<i class="fas fa-arrow-left"></i> {% translate "Back" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="nextBtn" class="nav-btn btn-next">
|
<button id="nextBtn" class="nav-btn btn-next">
|
||||||
{% trans "Next" %}
|
{% translate "Next" %}
|
||||||
<i class="fas fa-arrow-right"></i>
|
<i class="fas fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -529,7 +526,7 @@
|
|||||||
class="nav-btn btn-submit"
|
class="nav-btn btn-submit"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
>
|
>
|
||||||
{% trans "Submit Application" %}
|
{% translate "Submit Application" %}
|
||||||
<i class="fas fa-paper-plane"></i>
|
<i class="fas fa-paper-plane"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -237,7 +237,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="alert text-primary-theme border-0 shadow-sm mt-5" role="alert">
|
<div class="alert alert-info border-0 shadow-sm mt-5" role="alert">
|
||||||
<h5 class="alert-heading">{% trans "No Matching Opportunities" %}</h5>
|
<h5 class="alert-heading">{% trans "No Matching Opportunities" %}</h5>
|
||||||
<p>{% trans "We currently have no open roles that match your search and filters. Please modify your criteria or check back soon!" %}</p>
|
<p>{% trans "We currently have no open roles that match your search and filters. Please modify your criteria or check back soon!" %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -70,9 +70,15 @@
|
|||||||
|
|
||||||
{# 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">
|
||||||
{% comment %} <ul class="navbar-nav flex-row">
|
<ul class="navbar-nav flex-row">
|
||||||
<li class="nav-item me-2">
|
<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"
|
||||||
|
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">
|
||||||
@ -80,7 +86,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item me-2">
|
<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="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">
|
||||||
@ -90,7 +96,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul> {% endcomment %}
|
</ul>
|
||||||
|
|
||||||
<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 -->
|
||||||
@ -116,33 +122,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %} {% endcomment %}
|
{% endif %} {% endcomment %}
|
||||||
{% comment %} <li class="nav-item me-2">
|
<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
|
||||||
@ -160,12 +144,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.first_name }} {{ user.last_name }}
|
{{ user.username|first|upper }}
|
||||||
</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;"
|
||||||
>
|
>
|
||||||
@ -231,6 +215,10 @@
|
|||||||
<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>
|
||||||
@ -242,10 +230,11 @@
|
|||||||
{# 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">
|
||||||
<i class="fas fa-briefcase me-2"></i>
|
{% include "icons/jobs.html" %}
|
||||||
{% trans "Jobs" %}
|
{% trans "Jobs" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -253,7 +242,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">
|
||||||
<i class="fas fa-user-tie me-2"></i>
|
{% include "icons/users.html" %}
|
||||||
{% trans "Applications" %}
|
{% trans "Applications" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -261,7 +250,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">
|
||||||
<i class="fas fa-user me-2"></i>
|
{% include "icons/users.html" %}
|
||||||
{% trans "Applicant" %}
|
{% trans "Applicant" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -269,7 +258,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 me-2"></i>
|
<i class="fas fa-building"></i>
|
||||||
{% trans "Agencies" %}
|
{% trans "Agencies" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -277,21 +266,12 @@
|
|||||||
<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">
|
||||||
<i class="fas fa-calendar-check me-2"></i>
|
<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="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>
|
||||||
</li>
|
|
||||||
<li class="nav-item me-lg-4">
|
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'kaauh_career' %}active{% endif %}" href="{% url 'kaauh_career' %}">
|
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{% trans "Career Page" %}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
{% comment %} <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 == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
|
||||||
@ -303,18 +283,30 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li> {% endcomment %}
|
</li> {% endcomment %}
|
||||||
{% comment %} <li class="nav-item me-lg-4">
|
<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>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item me-lg-4">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'kaauh_career' %}active{% endif %}" href="{% url 'kaauh_career' %}">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{% trans "Career Page" %}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% comment %} <li class="nav-item dropdown ms-lg-2">
|
{% comment %} <li class="nav-item dropdown ms-lg-2">
|
||||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||||
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
||||||
|
|||||||
@ -226,7 +226,7 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# --- PARTICIPANTS --- #}
|
{# --- PARTICIPANTS --- #}
|
||||||
{% comment %} <div class="row g-4 mt-1 mb-5">
|
<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> {% endcomment %}
|
</div>
|
||||||
|
|
||||||
{# --- 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 crispy_forms_tags %}
|
{% load i18n static %}
|
||||||
|
|
||||||
{% 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,11 +219,6 @@
|
|||||||
|
|
||||||
<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">
|
||||||
@ -308,7 +303,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 "Assigned Staff" %}
|
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Staff" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item flex-fill" role="presentation">
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
@ -368,16 +363,20 @@
|
|||||||
{% 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-eye me-1"></i> {% trans "View" %}
|
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
|
||||||
</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" %}
|
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Form Template" %}
|
||||||
</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>
|
||||||
|
|
||||||
@ -385,47 +384,42 @@
|
|||||||
<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">
|
||||||
{% if job.assigned_to %}
|
|
||||||
<p class="text-muted small mb-3">
|
<p class="text-muted small mb-3">
|
||||||
<strong>{% trans "Assigned to:" %}</strong> {{ job.assigned_to }}
|
{% trans "Assign staff members to manage this job posting and track applications." %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
|
||||||
{% if not job.assigned_to %}
|
|
||||||
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
|
|
||||||
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
|
||||||
</button>
|
|
||||||
{% elif job.assigned_to and job.assigned_to == request.user %}
|
|
||||||
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
|
|
||||||
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Modal for Staff Assignment -->
|
<a href="{% url 'staff_assignment_view' job.slug %}" class="btn btn-main-action">
|
||||||
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
|
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
||||||
<div class="modal-dialog">
|
</a>
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
{% if job.assigned_to %}
|
||||||
<h5 class="modal-title" id="staffAssignmentModalLabel">
|
<div class="mt-3">
|
||||||
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
|
<h6 class="text-muted">{% trans "Current Assignments" %}</h6>
|
||||||
</h5>
|
{% for assignment in job.staff_assignments.all %}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<div class="card mb-2">
|
||||||
</div>
|
<div class="card-body p-2">
|
||||||
<div class="modal-body">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
|
<div>
|
||||||
{% csrf_token %}
|
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
|
||||||
{{staff_form|crispy}}
|
<br>
|
||||||
<div class="d-flex justify-content-end mt-3">
|
<small class="text-muted">{{ assignment.staff.email }}</small>
|
||||||
<button type="submit" class="btn btn-main-action">
|
</div>
|
||||||
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
|
<div>
|
||||||
</button>
|
{% if assignment.staff.is_active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
{% if assignment.notes %}
|
||||||
|
<small class="text-muted d-block mt-1">{{ assignment.notes }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
|
|
||||||
{% 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-primary-theme text-white">{{ meeting.type|title }}</span>
|
<span class="status-badge bg-{{ meeting.type }}">{{ 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-primary-theme text-white">
|
<span class="status-badge bg-{{ meeting.status }}">
|
||||||
{{ meeting.interview.get_status_display }}
|
{{ meeting.interview.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -216,15 +216,6 @@ 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 %}
|
||||||
@ -307,7 +298,7 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
{# --- PARTICIPANTS TABLE --- #}
|
{# --- PARTICIPANTS TABLE --- #}
|
||||||
{% comment %} <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-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>
|
||||||
@ -319,14 +310,6 @@ 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"
|
||||||
@ -369,7 +352,7 @@ body {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div> {% endcomment %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ========================================================= #}
|
{# ========================================================= #}
|
||||||
@ -377,7 +360,7 @@ body {
|
|||||||
{# ========================================================= #}
|
{# ========================================================= #}
|
||||||
<div class="row g-4 mt-1">
|
<div class="row g-4 mt-1">
|
||||||
|
|
||||||
{% comment %} <div class="col-lg-12">
|
<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);">
|
||||||
@ -386,6 +369,7 @@ 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 %}
|
||||||
@ -470,7 +454,7 @@ body {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> {% endcomment %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -486,9 +470,16 @@ 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>
|
||||||
@ -506,7 +497,10 @@ 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 %}{% trans "Reply to Message"%}{% else %}{% trans"Compose Message"%}{% endif %}{% endblock %}
|
{% block title %}{% if form.instance.pk %}Reply to Message{% else %}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 = `{% blocktrans %}{{ remaining }}/{{ maxLength }} characters{% endblocktrans %}`;
|
counter.textContent = `${subjectField.value.length}/${maxLength} characters`;
|
||||||
if (remaining < 20) {
|
if (remaining < 20) {
|
||||||
counter.className = 'text-warning';
|
counter.className = 'text-warning';
|
||||||
} else {
|
} else {
|
||||||
@ -236,4 +236,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -228,4 +228,3 @@ setInterval(() => {
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -252,15 +252,147 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{% url 'person_update' person.slug %}" enctype="multipart/form-data" id="person-form">
|
<form method="post" enctype="multipart/form-data" id="person-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{form|crispy}}
|
|
||||||
</form>
|
<!-- Profile Image Section -->
|
||||||
<div class="d-flex gap-2">
|
<div class="row mb-4">
|
||||||
<button form="person-form" type="submit" class="btn btn-main-action">
|
<div class="col-12">
|
||||||
<i class="fas fa-save me-1"></i> {% trans "Update" %}
|
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
|
||||||
</button>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -73,25 +73,8 @@
|
|||||||
</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">
|
||||||
@ -117,11 +100,20 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
|
||||||
|
|
||||||
{% comment %} <li class="nav-item dropdown">
|
<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>
|
||||||
@ -146,17 +138,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endcomment %}
|
|
||||||
{% 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 %}
|
||||||
@ -269,21 +250,6 @@
|
|||||||
} 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,13 +238,11 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate"%}</th>
|
<th>{% trans "Name" %}</th>
|
||||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
|
<th>{% trans "Contact" %}</th>
|
||||||
</th>
|
<th>{% trans "Stage" %}</th>
|
||||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
|
<th>{% trans "Submitted" %}</th>
|
||||||
</th>
|
<th>{% trans "Actions" %}</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>
|
||||||
@ -264,19 +262,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>
|
{{ candidate.created_at|date:"Y-m-d H:i" }}
|
||||||
{{candidate.email }}</div>
|
|
||||||
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4">
|
<td>
|
||||||
<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>
|
||||||
@ -326,9 +315,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="position-absolute top-50 start-50 translate-middle text-center">
|
<div class="progress-ring-text">
|
||||||
<div class="h3 fw-bold mb-0 text-dark">{{ total_candidates }}</div>
|
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
|
||||||
<div class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates}}</div>
|
{{ progress|floatformat:0 }}%
|
||||||
</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,9 +264,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'candidate_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
<button class="btn btn-sm btn-outline-primary" onclick="editCandidate({{ candidate.id }})" title="{% trans 'Edit Candidate' %}">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</button>
|
||||||
|
<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 person-table">
|
<div class="table-responsive">
|
||||||
<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>
|
||||||
{% comment %} <th scope="col">{% trans "Stage" %}</th>
|
<th scope="col">{% trans "Stage" %}</th>
|
||||||
<th scope="col">{% trans "Applied Date" %}</th> {% endcomment %}
|
<th scope="col">{% trans "Applied Date" %}</th>
|
||||||
<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>
|
||||||
{% comment %} <td>
|
<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> {% endcomment %}
|
</td>
|
||||||
<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">
|
||||||
<button type="button" data-bs-toggle="modal" data-bs-target="#updateModal"
|
<a href="{% url 'candidate_detail' person.slug %}"
|
||||||
hx-get="{% url 'person_update' person.slug %}"
|
class="btn btn-sm btn-outline-primary"
|
||||||
hx-target="#updateModalBody"
|
title="{% trans 'View Details' %}">
|
||||||
hx-swap="outerrHTML"
|
<i class="fas fa-eye"></i>
|
||||||
hx-select="#person-form"
|
</a>
|
||||||
hx-vals='{"view":"portal"}'
|
<button type="button"
|
||||||
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,28 +290,6 @@
|
|||||||
</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,9 +178,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid py-4">
|
||||||
<!-- 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">
|
||||||
@ -197,13 +195,12 @@
|
|||||||
</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">
|
||||||
@ -223,7 +220,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">
|
||||||
@ -278,9 +275,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>
|
||||||
@ -312,8 +309,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">
|
||||||
@ -330,15 +327,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>
|
||||||
@ -349,25 +346,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-primary-theme text-white">
|
<div class="card-header bg-success 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>
|
||||||
@ -384,28 +381,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.get_schedule_type == 'Remote' %}
|
{% if interview.zoom_meeting %}
|
||||||
<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-primary-theme">
|
<span class="badge bg-secondary">
|
||||||
<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="badg ">
|
<span class="badge bg-{{ interview.status|lower }} text-white">
|
||||||
{{ interview.get_schedule_status }}
|
{{ interview.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if interview.get_meeting_details and interview.get_schedule_type == 'Remote' %}
|
{% if interview.zoom_meeting and interview.zoom_meeting.join_url %}
|
||||||
<a href="{{ interview.get_meeting_details }}"
|
<a href="{{ interview.zoom_meeting.join_url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-sm bg-primary-theme text-white">
|
class="btn btn-sm btn-primary">
|
||||||
<i class="fas fa-video me-1"></i>
|
<i class="fas fa-video me-1"></i>
|
||||||
{% trans "Join" %}
|
{% trans "Join" %}
|
||||||
</a>
|
</a>
|
||||||
@ -439,7 +436,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">
|
||||||
@ -458,7 +455,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">
|
||||||
@ -478,7 +475,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>
|
||||||
@ -536,47 +533,6 @@
|
|||||||
{% 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">
|
||||||
@ -585,7 +541,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 }}
|
{{ candidate.name }}<i class="fas fa-eye ms-1"></i>
|
||||||
</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 i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{{ source.name }} - {% trans "Source Details" %}{% endblock %}
|
{% block title %}{{ source.name }} - 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> {% trans "Edit" %}
|
<i class="fas fa-edit"></i> 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,12 +24,13 @@
|
|||||||
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="{% blocktrans %}Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?{% endblocktrans %}">
|
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
|
||||||
|
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> {% trans "Delete" %}
|
<i class="fas fa-trash"></i> Delete
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,19 +40,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">{% trans "Source Information" %}</h6>
|
<h6 class="mb-0">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">{% trans "Name" %}</label>
|
<label class="form-label text-muted">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">{% trans "Type" %}</label>
|
<label class="form-label text-muted">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>
|
||||||
@ -61,7 +62,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 %}
|
||||||
@ -69,24 +70,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>
|
||||||
@ -96,24 +97,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">{% trans "Requires Authentication" %}</label>
|
<label class="form-label text-muted">Requires Authentication</label>
|
||||||
<div>
|
<div>
|
||||||
{% if source.requires_auth %}
|
{% if source.requires_auth %}
|
||||||
<span class="badge bg-warning">{% trans "Yes" %}</span>
|
<span class="badge bg-warning">Yes</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">{% trans "No" %}</span>
|
<span class="badge bg-secondary">No</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,21 +123,21 @@
|
|||||||
|
|
||||||
{% if source.webhook_url %}
|
{% if source.webhook_url %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted">{% trans "Webhook URL" %}</label>
|
<label class="form-label text-muted">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 %}
|
||||||
@ -144,13 +145,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>
|
||||||
@ -163,23 +164,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">{% trans "API Credentials" %}</h6>
|
<h6 class="mb-0">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">{% trans "API Key" %}</label>
|
<label class="form-label text-muted">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="{% trans "Copy to clipboard" %}">
|
title="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">{% trans "API Secret" %}</label>
|
<label class="form-label text-muted">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()">
|
||||||
@ -188,14 +189,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="{% trans "Copy to clipboard" %}">
|
title="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> {% trans "Generate New Keys" %}
|
<i class="fas fa-key"></i> Generate New Keys
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -204,24 +205,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">{% trans "Integration Statistics" %}</h6>
|
<h6 class="mb-0">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">{% trans "Total API Calls" %}</label>
|
<label class="form-label text-muted">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">{% trans "Successful Calls" %}</label>
|
<label class="form-label text-muted">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">{% trans "Failed Calls" %}</label>
|
<label class="form-label text-muted">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">{% trans "Success Rate" %}</label>
|
<label class="form-label text-muted">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>
|
||||||
@ -235,8 +236,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">{% trans "Recent Integration Logs" %}</h6>
|
<h6 class="mb-0">Recent Integration Logs</h6>
|
||||||
<small class="text-muted">{% trans "Last 10 logs" %}</small>
|
<small class="text-muted">Last 10 logs</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if integration_logs %}
|
{% if integration_logs %}
|
||||||
@ -244,11 +245,11 @@
|
|||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Timestamp" %}</th>
|
<th>Timestamp</th>
|
||||||
<th>{% trans "Method" %}</th>
|
<th>Method</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>Status</th>
|
||||||
<th>{% trans "Response Time" %}</th>
|
<th>Response Time</th>
|
||||||
<th>{% trans "Details" %}</th>
|
<th>Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -285,7 +286,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>
|
||||||
@ -296,7 +297,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>
|
||||||
@ -312,24 +313,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">{% trans "Integration Log Details" %}</h5>
|
<h5 class="modal-title">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 %}
|
||||||
@ -339,7 +340,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 %}
|
||||||
@ -349,24 +350,25 @@
|
|||||||
</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