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