update translation and more
This commit is contained in:
parent
e1a55d7633
commit
852eac84bf
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
10791
locale/ar/LC_MESSAGES/django-bkp.po
Normal file
10791
locale/ar/LC_MESSAGES/django-bkp.po
Normal file
File diff suppressed because it is too large
Load Diff
11316
locale/ar/LC_MESSAGES/django-old.po
Normal file
11316
locale/ar/LC_MESSAGES/django-old.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
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."))
|
||||
@ -130,7 +130,8 @@ from .models import (
|
||||
Message,
|
||||
Document,
|
||||
InterviewLocation,
|
||||
InterviewNote
|
||||
InterviewNote,
|
||||
OnsiteLocationDetails
|
||||
)
|
||||
|
||||
|
||||
@ -211,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
|
||||
@ -3086,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)
|
||||
@ -3286,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)
|
||||
|
||||
@ -3344,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)
|
||||
|
||||
|
||||
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)
|
||||
@ -144,17 +144,6 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item ms-3">
|
||||
{% if request.user.is_authenticated %}
|
||||
<form method="post" action="{% url 'account_logout' %}" class="d-inline py-2 d-flex align-items-center">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-light btn-sm">
|
||||
<i class="fas fa-sign-out-alt me-1"></i> {% trans "Logout" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link p-0 border-0 bg-transparent dropdown-toggle"
|
||||
|
||||
@ -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">
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -377,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);">
|
||||
@ -386,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 %}
|
||||
@ -471,7 +470,7 @@ body {
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -487,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>
|
||||
@ -514,10 +506,7 @@ body {
|
||||
{{ form.system_users }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
||||
@ -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">
|
||||
@ -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>
|
||||
|
||||
@ -267,12 +267,6 @@
|
||||
<a href="{% url 'candidate_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editCandidate({{ candidate.id }})" title="{% trans 'Edit Candidate' %}">
|
||||
<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>
|
||||
</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">
|
||||
|
||||
@ -573,25 +573,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% comment %} <div class="col-md-3 mb-3">
|
||||
<div class="kaauh-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-edit fa-2x text-warning mb-3"></i>
|
||||
<h6>{% trans "Update Profile" %}</h6>
|
||||
<p class="text-muted small">{% trans "Edit your personal information" %}</p>
|
||||
<a href="" class="btn btn-main-action w-100">
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
{% trans "Update" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
|
||||
<!-- Next Steps Section -->
|
||||
{% comment %} <div class="row">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="kaauh-card">
|
||||
<div class="card-header bg-primary-theme text-white">
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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