Merge branch 'update1'

This commit is contained in:
ismail 2025-11-13 15:56:59 +03:00
commit 9497bf102e
102 changed files with 36358 additions and 3518 deletions

View File

@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
from django.templatetags.static import static
@ -22,7 +23,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-_!ew&)1&r--3h17knd27^x8(xu(&-f4q3%x543lv5vx2!784s*'
SECRET_KEY = "django-insecure-_!ew&)1&r--3h17knd27^x8(xu(&-f4q3%x543lv5vx2!784s*"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -32,104 +33,102 @@ ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.humanize',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'recruitment.apps.RecruitmentConfig',
'corsheaders',
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.linkedin_oauth2',
'channels',
'django_filters',
'crispy_forms',
"django.contrib.admin",
"django.contrib.humanize",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"recruitment.apps.RecruitmentConfig",
"corsheaders",
"django.contrib.sites",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.linkedin_oauth2",
"channels",
"django_filters",
"crispy_forms",
# 'django_summernote',
# 'ckeditor',
'django_ckeditor_5',
'crispy_bootstrap5',
'django_extensions',
'template_partials',
'django_countries',
'django_celery_results',
'django_q',
'widget_tweaks',
'easyaudit'
"django_ckeditor_5",
"crispy_bootstrap5",
"django_extensions",
"template_partials",
"django_countries",
"django_celery_results",
"django_q",
"widget_tweaks",
"easyaudit",
]
SITE_ID = 1
LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = "/"
ACCOUNT_SIGNUP_REDIRECT_URL = '/'
ACCOUNT_SIGNUP_REDIRECT_URL = "/"
LOGIN_URL = '/accounts/login/'
LOGIN_URL = "/accounts/login/"
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
'easyaudit.middleware.easyaudit.EasyAuditMiddleware',
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware",
"easyaudit.middleware.easyaudit.EasyAuditMiddleware",
]
ROOT_URLCONF = 'NorahUniversity.urls'
ROOT_URLCONF = "NorahUniversity.urls"
CORS_ALLOW_ALL_ORIGINS = True
ASGI_APPLICATION = 'hospital_recruitment.asgi.application'
ASGI_APPLICATION = "hospital_recruitment.asgi.application"
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
WSGI_APPLICATION = "NorahUniversity.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
@ -157,6 +156,23 @@ DATABASES = {
# AUTH_PASSWORD_VALIDATORS = [
# {
# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
# },
# {
# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
# },
# {
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
# },
# {
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
# },
# ]
# settings.py
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@ -173,21 +189,20 @@ AUTH_PASSWORD_VALIDATORS = [
]
ACCOUNT_LOGIN_METHODS = ['email']
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
ACCOUNT_LOGIN_METHODS = ["email"]
ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_FORMS = {'signup': 'recruitment.forms.StaffSignupForm'}
ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"}
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Crispy Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
@ -195,29 +210,29 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
# Bootstrap 5 Configuration
CRISPY_BS5 = {
'include_placeholder_text': True,
'use_css_helpers': True,
"include_placeholder_text": True,
"use_css_helpers": True,
}
ACCOUNT_RATE_LIMITS = {
'send_email_confirmation': None, # Disables the limit
"send_email_confirmation": None, # Disables the limit
}
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGES = [
('en', 'English'),
('ar', 'Arabic'),
("en", "English"),
("ar", "Arabic"),
]
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
LOCALE_PATHS = [
BASE_DIR / 'locale',
BASE_DIR / "locale",
]
TIME_ZONE = 'Asia/Riyadh'
TIME_ZONE = "Asia/Riyadh"
USE_I18N = True
@ -226,36 +241,35 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
STATICFILES_DIRS = [
BASE_DIR / 'static'
]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
MEDIA_ROOT = os.path.join(BASE_DIR, 'static/media')
STATIC_URL = "/static/"
MEDIA_URL = "/media/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# LinkedIn OAuth Config
SOCIALACCOUNT_PROVIDERS = {
'linkedin_oauth2': {
'SCOPE': [
'r_liteprofile', 'r_emailaddress', 'w_member_social',
'rw_organization_admin', 'w_organization_social'
"linkedin_oauth2": {
"SCOPE": [
"r_liteprofile",
"r_emailaddress",
"w_member_social",
"rw_organization_admin",
"w_organization_social",
],
'PROFILE_FIELDS': [
'id', 'first-name', 'last-name', 'email-address'
]
"PROFILE_FIELDS": ["id", "first-name", "last-name", "email-address"],
}
}
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
SECRET_TOKEN = '6KdTGyF0SSCSL_V4Xa34aw'
ZOOM_ACCOUNT_ID = "HoGikHXsQB2GNDC5Rvyw9A"
ZOOM_CLIENT_ID = "brC39920R8C8azfudUaQgA"
ZOOM_CLIENT_SECRET = "rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L"
SECRET_TOKEN = "6KdTGyF0SSCSL_V4Xa34aw"
ZOOM_WEBHOOK_API_KEY = "2GNDC5Rvyw9AHoGikHXsQB"
# Maximum file upload size (in bytes)
@ -264,145 +278,199 @@ FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
CORS_ALLOW_CREDENTIALS = True
CELERY_BROKER_URL = 'redis://localhost:6379/0' # Or your message broker URL
CELERY_RESULT_BACKEND = 'django-db' # If using django-celery-results
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_BROKER_URL = "redis://localhost:6379/0" # Or your message broker URL
CELERY_RESULT_BACKEND = "django-db" # If using django-celery-results
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_TIMEZONE = "UTC"
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
LINKEDIN_CLIENT_ID = "867jwsiyem1504"
LINKEDIN_CLIENT_SECRET = "WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=="
LINKEDIN_REDIRECT_URI = "http://127.0.0.1:8000/jobs/linkedin/callback/"
Q_CLUSTER = {
'name': 'KAAUH_CLUSTER',
'workers': 8,
'recycle': 500,
'timeout': 60,
'max_attempts': 1,
'compress': True,
'save_limit': 250,
'queue_limit': 500,
'cpu_affinity': 1,
'label': 'Django Q2',
'redis': {
'host': '127.0.0.1',
'port': 6379,
'db': 3, },
'ALT_CLUSTERS': {
'long': {
'timeout': 3000,
'retry': 3600,
'max_attempts': 2,
"name": "KAAUH_CLUSTER",
"workers": 8,
"recycle": 500,
"timeout": 60,
"max_attempts": 1,
"compress": True,
"save_limit": 250,
"queue_limit": 500,
"cpu_affinity": 1,
"label": "Django Q2",
"redis": {
"host": "127.0.0.1",
"port": 6379,
"db": 3,
},
"ALT_CLUSTERS": {
"long": {
"timeout": 3000,
"retry": 3600,
"max_attempts": 2,
},
'short': {
'timeout': 10,
'max_attempts': 1,
"short": {
"timeout": 10,
"max_attempts": 1,
},
}
},
}
customColorPalette = [
{
'color': 'hsl(4, 90%, 58%)',
'label': 'Red'
},
{
'color': 'hsl(340, 82%, 52%)',
'label': 'Pink'
},
{
'color': 'hsl(291, 64%, 42%)',
'label': 'Purple'
},
{
'color': 'hsl(262, 52%, 47%)',
'label': 'Deep Purple'
},
{
'color': 'hsl(231, 48%, 48%)',
'label': 'Indigo'
},
{
'color': 'hsl(207, 90%, 54%)',
'label': 'Blue'
},
]
{"color": "hsl(4, 90%, 58%)", "label": "Red"},
{"color": "hsl(340, 82%, 52%)", "label": "Pink"},
{"color": "hsl(291, 64%, 42%)", "label": "Purple"},
{"color": "hsl(262, 52%, 47%)", "label": "Deep Purple"},
{"color": "hsl(231, 48%, 48%)", "label": "Indigo"},
{"color": "hsl(207, 90%, 54%)", "label": "Blue"},
]
# CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional
# CKEDITOR_5_FILE_STORAGE = "path_to_storage.CustomStorage" # optional
CKEDITOR_5_CONFIGS = {
'default': {
'toolbar': {
'items': ['heading', '|', 'bold', 'italic', 'link',
'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ],
}
"default": {
"toolbar": {
"items": [
"heading",
"|",
"bold",
"italic",
"link",
"bulletedList",
"numberedList",
"blockQuote",
"imageUpload",
],
}
},
'extends': {
'blockToolbar': [
'paragraph', 'heading1', 'heading2', 'heading3',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote',
"extends": {
"blockToolbar": [
"paragraph",
"heading1",
"heading2",
"heading3",
"|",
"bulletedList",
"numberedList",
"|",
"blockQuote",
],
'toolbar': {
'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
'code','subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|',
'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
'insertTable',
],
'shouldNotGroupWhenFull': 'true'
"toolbar": {
"items": [
"heading",
"|",
"outdent",
"indent",
"|",
"bold",
"italic",
"link",
"underline",
"strikethrough",
"code",
"subscript",
"superscript",
"highlight",
"|",
"codeBlock",
"sourceEditing",
"insertImage",
"bulletedList",
"numberedList",
"todoList",
"|",
"blockQuote",
"imageUpload",
"|",
"fontSize",
"fontFamily",
"fontColor",
"fontBackgroundColor",
"mediaEmbed",
"removeFormat",
"insertTable",
],
"shouldNotGroupWhenFull": "true",
},
'image': {
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
'styles': [
'full',
'side',
'alignLeft',
'alignRight',
'alignCenter',
]
"image": {
"toolbar": [
"imageTextAlternative",
"|",
"imageStyle:alignLeft",
"imageStyle:alignRight",
"imageStyle:alignCenter",
"imageStyle:side",
"|",
],
"styles": [
"full",
"side",
"alignLeft",
"alignRight",
"alignCenter",
],
},
'table': {
'contentToolbar': [ 'tableColumn', 'tableRow', 'mergeTableCells',
'tableProperties', 'tableCellProperties' ],
'tableProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
"table": {
"contentToolbar": [
"tableColumn",
"tableRow",
"mergeTableCells",
"tableProperties",
"tableCellProperties",
],
"tableProperties": {
"borderColors": customColorPalette,
"backgroundColors": customColorPalette,
},
"tableCellProperties": {
"borderColors": customColorPalette,
"backgroundColors": customColorPalette,
},
'tableCellProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
}
},
'heading' : {
'options': [
{ 'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph' },
{ 'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1' },
{ 'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2' },
{ 'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3' }
"heading": {
"options": [
{
"model": "paragraph",
"title": "Paragraph",
"class": "ck-heading_paragraph",
},
{
"model": "heading1",
"view": "h1",
"title": "Heading 1",
"class": "ck-heading_heading1",
},
{
"model": "heading2",
"view": "h2",
"title": "Heading 2",
"class": "ck-heading_heading2",
},
{
"model": "heading3",
"view": "h3",
"title": "Heading 3",
"class": "ck-heading_heading3",
},
]
},
},
"list": {
"properties": {
"styles": "true",
"startIndex": "true",
"reversed": "true",
}
},
'list': {
'properties': {
'styles': 'true',
'startIndex': 'true',
'reversed': 'true',
}
}
}
# Define a constant in settings.py to specify file upload permissions
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any"
CKEDITOR_5_FILE_UPLOAD_PERMISSION = (
"staff" # Possible values: "staff", "authenticated", "any"
@ -411,3 +479,7 @@ from django.contrib.messages import constants as messages
MESSAGE_TAGS = {
messages.ERROR: 'danger',
}
)
# Custom User Model
AUTH_USER_MODEL = "recruitment.CustomUser"

View File

@ -26,6 +26,7 @@ urlpatterns = [
path('application/<slug:template_slug>/', views.application_submit_form, name='application_submit_form'),
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'),
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
path('application/<slug:slug>/signup/', views.candidate_signup, name='candidate_signup'),
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
path('application/applicant/profile', views.applicant_profile, name='applicant_profile'),

119
apply_all_translations.py Normal file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Script to apply all batch translations to the main django.po file
"""
import re
def apply_all_translations():
"""
Apply all translations to the main django.po file
"""
# All translations from batches 02-35
translations = {
"The date and time this notification is scheduled to be sent.": "التاريخ والوقت المحدد لإرسال هذا الإشعار.",
"Send Attempts": "محاولات الإرسال",
"Failed to start the job posting process. Please try again.": "فشل في بدء عملية نشر الوظيفة. يرجى المحاولة مرة أخرى.",
"You don't have permission to view this page.": "ليس لديك إذن لعرض هذه الصفحة.",
"Account Inactive": "الحساب غير نشط",
"Princess Nourah bint Abdulrahman University": "جامعة الأميرة نورة بنت عبدالرحمن",
"Manage your personal details and security.": "إدارة تفاصيلك الشخصية والأمان.",
"Primary": "أساسي",
"Verified": "موثق",
"Unverified": "غير موثق",
"Make Primary": "جعل أساسي",
"Remove": "إزالة",
"Add Email Address": "إضافة عنوان بريد إلكتروني",
"Hello,": "مرحباً،",
"Confirm My KAAUH ATS Email": "تأكيد بريدي الإلكتروني في نظام توظيف جامعة نورة",
"Alternatively, copy and paste this link into your browser:": "بدلاً من ذلك، انسخ والصق هذا الرابط في متصفحك:",
"Password Reset Request": "طلب إعادة تعيين كلمة المرور",
"Click Here to Reset Your Password": "اضغط هنا لإعادة تعيين كلمة المرور",
"This link is only valid for a limited time.": "هذا الرابط صالح لفترة محدودة فقط.",
"Thank you,": "شكراً لك،",
"KAAUH ATS Team": "فريق نظام توظيف جامعة نورة",
"Confirm Email Address": "تأكيد عنوان البريد الإلكتروني",
"Account Verification": "التحقق من الحساب",
"Verify your email to secure your account and unlock full features.": "تحقق من بريدك الإلكتروني لتأمين حسابك وإلغاء قفل جميع الميزات.",
"Confirm Your Email Address": "تأكيد عنوان بريدك الإلكتروني",
"Verification Failed": "فشل التحقق",
"The email confirmation link is expired or invalid.": "رابط تأكيد البريد الإلكتروني منتهي الصلاحية أو غير صالح.",
"Keep me signed in": "ابق مسجلاً للدخول",
"Return to Profile": "العودة إلى الملف الشخصي",
"Enter your e-mail address to reset your password.": "أدخل عنوان بريدك الإلكتروني لإعادة تعيين كلمة المرور.",
"Remember your password?": "تتذكر كلمة المرور؟",
"Log In": "تسجيل الدخول",
"Password Reset Sent": "تم إرسال إعادة تعيين كلمة المرور",
"Return to Login": "العودة إلى تسجيل الدخول",
"Please enter your new password below.": "يرجى إدخال كلمة المرور الجديدة أدناه.",
"Logout": "تسجيل الخروج",
"Yes": "نعم",
"No": "لا",
"Success": "نجح",
"Login": "تسجيل الدخول",
"Link": "ربط",
"Clear": "مسح",
"All": "الكل",
"Comments": "التعليقات",
"Save": "حفظ",
"Notes": "ملاحظات",
"New": "جديد",
"Users": "المستخدمون",
"Filter": "تصفية",
"Home": "الرئيسية",
"Username": "اسم المستخدم",
"Modified": "تم التعديل",
"Unlink": "فك الربط",
"Group": "تجميع",
"Export": "تصدير",
"Import": "استيراد",
"None": "لا شيء",
"Add": "إضافة",
"True": "صحيح",
"False": "خطأ",
}
main_po_file = "locale/ar/LC_MESSAGES/django.po"
# Read the main django.po file
with open(main_po_file, 'r', encoding='utf-8') as f:
main_content = f.read()
# Apply translations to main file
updated_content = main_content
applied_count = 0
for english, arabic in translations.items():
# Pattern to find msgid followed by empty msgstr
pattern = rf'(msgid "{re.escape(english)}"\s*\nmsgstr) ""'
replacement = rf'\1 "{arabic}"'
if re.search(pattern, updated_content):
updated_content = re.sub(pattern, replacement, updated_content)
applied_count += 1
print(f"✓ Applied: '{english}' -> '{arabic}'")
else:
print(f"✗ Not found: '{english}'")
# Write updated content back to main file
with open(main_po_file, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f"\nApplied {applied_count} translations to {main_po_file}")
return applied_count
def main():
"""Main function to apply all translations"""
print("Applying all batch translations to main django.po file...")
applied_count = apply_all_translations()
if applied_count > 0:
print(f"\n✅ Successfully applied {applied_count} translations!")
print("Next steps:")
print("1. Run: python manage.py compilemessages")
print("2. Test the translations in the application")
else:
print("\n❌ No translations were applied.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Script to apply completed batch translations back to the main django.po file
"""
import re
def apply_batch_to_main_po(batch_file_path, main_po_path):
"""
Apply translations from a completed batch file to the main django.po file
"""
# Read the completed batch file
with open(batch_file_path, 'r', encoding='utf-8') as f:
batch_content = f.read()
# Extract translations from batch file
translations = {}
current_msgid = None
lines = batch_content.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('msgid: "'):
current_msgid = line[7:-1] # Extract msgid content
# Look for the Arabic translation line (next non-empty line after "Arabic Translation:")
j = i + 1
while j < len(lines) and not lines[j].strip().startswith('msgstr: "'):
j += 1
if j < len(lines) and lines[j].strip().startswith('msgstr: "'):
translation = lines[j].strip()[8:-1] # Extract msgstr content
if translation and current_msgid and translation != '""': # Only add non-empty translations
translations[current_msgid] = translation
print(f"Extracted: '{current_msgid}' -> '{translation}'")
current_msgid = None
i += 1
print(f"Found {len(translations)} translations to apply")
# Read the main django.po file
with open(main_po_path, 'r', encoding='utf-8') as f:
main_content = f.read()
# Apply translations to main file
updated_content = main_content
applied_count = 0
for english, arabic in translations.items():
# Pattern to find msgid followed by empty msgstr
pattern = rf'(msgid "{re.escape(english)}"\s*\nmsgstr) ""'
replacement = rf'\1 "{arabic}"'
if re.search(pattern, updated_content):
updated_content = re.sub(pattern, replacement, updated_content)
applied_count += 1
print(f"✓ Applied: '{english}' -> '{arabic}'")
else:
print(f"✗ Not found: '{english}'")
# Write updated content back to main file
with open(main_po_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f"\nApplied {applied_count} translations to {main_po_path}")
return applied_count
def main():
"""Main function to apply batch 01 translations"""
batch_file = "translation_batch_01_completed.txt"
main_po_file = "locale/ar/LC_MESSAGES/django.po"
print("Applying Batch 01 translations to main django.po file...")
applied_count = apply_batch_to_main_po(batch_file, main_po_file)
if applied_count > 0:
print(f"\n✅ Successfully applied {applied_count} translations!")
print("Next steps:")
print("1. Run: python manage.py compilemessages")
print("2. Test the translations in the application")
print("3. Continue with the next batch")
else:
print("\n❌ No translations were applied. Please check the batch file format.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Script to directly apply Arabic translations to the main django.po file
"""
import re
def apply_translations_direct():
"""
Apply translations directly to the main django.po file
"""
# Arabic translations for batch 01
translations = {
"Website": "الموقع الإلكتروني",
"Admin Notes": "ملاحظات المسؤول",
"Save Assignment": "حفظ التكليف",
"Assignment": "التكليف",
"Expires At": "ينتهي في",
"Access Token": "رمز الوصول",
"Subject": "الموضوع",
"Recipients": "المستلمون",
"Internal staff involved in the recruitment process for this job": "الموظفون الداخليون المشاركون في عملية التوظيف لهذه الوظيفة",
"External Participant": "مشارك خارجي",
"External participants involved in the recruitment process for this job": "المشاركون الخارجيون المشاركون في عملية التوظيف لهذه الوظيفة",
"Reason for canceling the job posting": "سبب إلغاء نشر الوظيفة",
"Name of person who cancelled this job": "اسم الشخص الذي ألغى هذه الوظيفة",
"Hired": "تم التوظيف",
"Author": "المؤلف",
"Endpoint URL for sending candidate data (for outbound sync)": "عنوان URL لنقطة النهاية لإرسال بيانات المرشح (للمزامنة الصادرة)",
"HTTP method for outbound sync requests": "طريقة HTTP لطلبات المزامنة الصادرة",
"HTTP method for connection testing": "طريقة HTTP لاختبار الاتصال",
"Custom Headers": "رؤوس مخصصة",
"JSON object with custom HTTP headers for sync requests": "كائن JSON يحتوي على رؤوس HTTP مخصصة لطلبات المزامنة",
"Supports Outbound Sync": "يدعم المزامنة الصادرة",
"Whether this source supports receiving candidate data from ATS": "ما إذا كان هذا المصدر يدعم استقبال بيانات المرشح من نظام تتبع المتقدمين",
"Expired": "منتهي الصلاحية",
"Maximum candidates agency can submit for this job": "الحد الأقصى للمرشحين الذين يمكن للوكالة تقديمهم لهذه الوظيفة"
}
main_po_file = "locale/ar/LC_MESSAGES/django.po"
# Read the main django.po file
with open(main_po_file, 'r', encoding='utf-8') as f:
main_content = f.read()
# Apply translations to main file
updated_content = main_content
applied_count = 0
for english, arabic in translations.items():
# Pattern to find msgid followed by empty msgstr
pattern = rf'(msgid "{re.escape(english)}"\s*\nmsgstr) ""'
replacement = rf'\1 "{arabic}"'
if re.search(pattern, updated_content):
updated_content = re.sub(pattern, replacement, updated_content)
applied_count += 1
print(f"✓ Applied: '{english}' -> '{arabic}'")
else:
print(f"✗ Not found: '{english}'")
# Write updated content back to main file
with open(main_po_file, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f"\nApplied {applied_count} translations to {main_po_file}")
return applied_count
def main():
"""Main function to apply batch 01 translations"""
print("Applying Batch 01 translations directly to main django.po file...")
applied_count = apply_translations_direct()
if applied_count > 0:
print(f"\n✅ Successfully applied {applied_count} translations!")
print("Next steps:")
print("1. Run: python manage.py compilemessages")
print("2. Test the translations in the application")
print("3. Continue with the next batch")
else:
print("\n❌ No translations were applied.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Comprehensive Translation Merger
Merges all 35 translation batch files into the main django.po file
"""
import os
import re
import glob
def parse_batch_file(filename):
"""Parse a batch file and extract English-Arabic translation pairs"""
translations = {}
try:
with open(filename, 'r', encoding='utf-8') as f:
content = f.read()
# Pattern to match the format in completed batch files:
# msgid: "English text"
# msgstr: ""
# Arabic Translation:
# msgstr: "Arabic text"
pattern = r'msgid:\s*"([^"]*?)"\s*\nmsgstr:\s*""\s*\nArabic Translation:\s*\nmsgstr:\s*"([^"]*?)"'
matches = re.findall(pattern, content, re.MULTILINE | re.DOTALL)
for english, arabic in matches:
english = english.strip()
arabic = arabic.strip()
# Skip empty or invalid entries
if english and arabic and len(english) > 1 and len(arabic) > 1:
translations[english] = arabic
except Exception as e:
print(f"Error parsing {filename}: {e}")
return translations
def parse_current_django_po():
"""Parse the current django.po file and extract existing translations"""
po_file = 'locale/ar/LC_MESSAGES/django.po'
if not os.path.exists(po_file):
return {}, []
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
# Extract msgid/msgstr pairs
pattern = r'msgid\s+"([^"]*?)"\s*\nmsgstr\s+"([^"]*?)"'
matches = re.findall(pattern, content)
existing_translations = {}
for msgid, msgstr in matches:
existing_translations[msgid] = msgstr
# Extract the header and footer
parts = re.split(r'(msgid\s+"[^"]*?"\s*\nmsgstr\s+"[^"]*?")', content)
return existing_translations, parts
def create_comprehensive_translation_dict():
"""Create a comprehensive translation dictionary from all batch files"""
all_translations = {}
# Get all batch files
batch_files = glob.glob('translation_batch_*.txt')
batch_files.sort() # Process in order
print(f"Found {len(batch_files)} batch files")
for batch_file in batch_files:
print(f"Processing {batch_file}...")
batch_translations = parse_batch_file(batch_file)
for english, arabic in batch_translations.items():
if english not in all_translations:
all_translations[english] = arabic
else:
# Keep the first translation found, but note duplicates
print(f" Duplicate found: '{english}' -> '{arabic}' (existing: '{all_translations[english]}')")
print(f"Total unique translations: {len(all_translations)}")
return all_translations
def update_django_po(translations):
"""Update the django.po file with new translations"""
po_file = 'locale/ar/LC_MESSAGES/django.po'
# Read current file
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
new_lines = []
i = 0
updated_count = 0
while i < len(lines):
line = lines[i]
if line.startswith('msgid '):
# Extract the msgid content
msgid_match = re.match(r'msgid\s+"([^"]*)"', line)
if msgid_match:
msgid = msgid_match.group(1)
# Look for the corresponding msgstr
if i + 1 < len(lines) and lines[i + 1].startswith('msgstr '):
msgstr_match = re.match(r'msgstr\s+"([^"]*)"', lines[i + 1])
current_msgstr = msgstr_match.group(1) if msgstr_match else ""
# Check if we have a translation for this msgid
if msgid in translations and (not current_msgstr or current_msgstr == ""):
# Update the translation
new_translation = translations[msgid]
new_lines.append(line) # Keep msgid line
new_lines.append(f'msgstr "{new_translation}"') # Update msgstr
updated_count += 1
print(f" Updated: '{msgid}' -> '{new_translation}'")
else:
# Keep existing translation
new_lines.append(line)
new_lines.append(lines[i + 1])
i += 2 # Skip both msgid and msgstr lines
continue
new_lines.append(line)
i += 1
# Write updated content
new_content = '\n'.join(new_lines)
# Create backup
backup_file = po_file + '.backup'
with open(backup_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Created backup: {backup_file}")
# Write updated file
with open(po_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Updated {updated_count} translations in {po_file}")
return updated_count
def add_missing_translations(translations):
"""Add completely missing translations to django.po"""
po_file = 'locale/ar/LC_MESSAGES/django.po'
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
existing_translations, _ = parse_current_django_po()
# Find translations that don't exist in the .po file at all
missing_translations = {}
for english, arabic in translations.items():
if english not in existing_translations:
missing_translations[english] = arabic
if missing_translations:
print(f"Found {len(missing_translations)} completely missing translations")
# Add missing translations to the end of the file
with open(po_file, 'a', encoding='utf-8') as f:
f.write('\n\n# Auto-added missing translations\n')
for english, arabic in missing_translations.items():
f.write(f'\nmsgid "{english}"\n')
f.write(f'msgstr "{arabic}"\n')
print(f"Added {len(missing_translations)} missing translations")
else:
print("No missing translations found")
return len(missing_translations)
def main():
"""Main function to merge all translations"""
print("🚀 Starting Comprehensive Translation Merger")
print("=" * 50)
# Step 1: Create comprehensive translation dictionary
print("\n📚 Step 1: Building comprehensive translation dictionary...")
translations = create_comprehensive_translation_dict()
# Step 2: Update existing translations in django.po
print("\n🔄 Step 2: Updating existing translations in django.po...")
updated_count = update_django_po(translations)
# Step 3: Add completely missing translations
print("\n Step 3: Adding missing translations...")
added_count = add_missing_translations(translations)
# Step 4: Summary
print("\n📊 Summary:")
print(f" Total translations available: {len(translations)}")
print(f" Updated existing translations: {updated_count}")
print(f" Added missing translations: {added_count}")
print(f" Total translations processed: {updated_count + added_count}")
print("\n✅ Translation merge completed!")
print("\n📝 Next steps:")
print(" 1. Run: python manage.py compilemessages")
print(" 2. Test Arabic translations in the browser")
print(" 3. Verify language switching functionality")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,48 @@
EMPTY TRANSLATIONS SUMMARY REPORT
==================================================
Total empty translations: 843
UI Elements (Buttons, Links): 20
Form Fields & Inputs: 55
Messages (Error/Success/Warning): 27
Navigation & Pages: 7
Other: 734
SAMPLE ENTRIES:
------------------------------
UI Elements (showing first 5):
Line 1491: "Click Here to Reset Your Password"
Line 2685: "Email will be sent to all selected recipients"
Line 2743: "Click here to join meeting"
Line 2813: "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)"
Line 4057: "Select the agency job assignment"
Form Fields (showing first 5):
Line 1658: "Enter your e-mail address to reset your password."
Line 1712: "Please enter your new password below."
Line 2077: "Form:"
Line 2099: "Field Property"
Line 2133: "Field Required"
Messages (showing first 5):
Line 1214: "Notification Message"
Line 2569: "Success"
Line 2776: "An unknown error occurred."
Line 2780: "An error occurred while processing your request."
Line 2872: "Your application has been submitted successfully"
Navigation (showing first 5):
Line 1295: "You don't have permission to view this page."
Line 2232: "Page"
Line 6253: "Admin Settings Dashboard"
Line 6716: "That page number is not an integer"
Line 6720: "That page number is less than 1"
Other (showing first 5):
Line 7: ""
Line 1041: "Number of candidates submitted so far"
Line 1052: "Deadline for agency to submit candidates"
Line 1068: "Original deadline before extensions"
Line 1078: "Agency Job Assignment"

173
find_empty_translations.py Normal file
View File

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
Script to find empty msgstr entries in django.po file and organize them into batches
for systematic Arabic translation work.
"""
import re
import os
from typing import List, Tuple
def find_empty_translations(po_file_path: str) -> List[Tuple[int, str, str]]:
"""
Find all entries with empty msgstr in the django.po file.
Returns:
List of tuples: (line_number, msgid, context_before)
"""
empty_translations = []
with open(po_file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
# Look for msgid
if line.startswith('msgid '):
msgid = line[7:-1] # Remove 'msgid "' and ending '"'
# Check next few lines for msgstr
j = i + 1
msgstr_found = False
msgstr_empty = False
while j < len(lines) and j < i + 5: # Look ahead max 5 lines
next_line = lines[j].strip()
if next_line.startswith('msgstr '):
msgstr_found = True
if next_line == 'msgstr ""':
msgstr_empty = True
break
elif next_line.startswith('msgid ') or next_line.startswith('#'):
# Found next entry or comment, no msgstr for current msgid
break
j += 1
if msgstr_found and msgstr_empty:
# Get context (previous 2-3 lines)
context_start = max(0, i - 3)
context = ''.join(lines[context_start:i])
empty_translations.append((i + 1, msgid, context))
i = j # Skip to after msgstr
else:
i += 1
return empty_translations
def create_batch_files(empty_translations: List[Tuple[int, str, str]], batch_size: int = 25):
"""
Create batch files with empty translations for systematic work.
"""
total_batches = (len(empty_translations) + batch_size - 1) // batch_size
print(f"Found {len(empty_translations)} empty translations")
print(f"Creating {total_batches} batches of ~{batch_size} translations each")
for batch_num in range(total_batches):
start_idx = batch_num * batch_size
end_idx = min(start_idx + batch_size, len(empty_translations))
batch_translations = empty_translations[start_idx:end_idx]
batch_filename = f"translation_batch_{batch_num + 1:02d}.txt"
with open(batch_filename, 'w', encoding='utf-8') as batch_file:
batch_file.write(f"=== TRANSLATION BATCH {batch_num + 1:02d} ===\n")
batch_file.write(f"Translations {start_idx + 1}-{end_idx} of {len(empty_translations)}\n")
batch_file.write("=" * 60 + "\n\n")
for line_num, msgid, context in batch_translations:
batch_file.write(f"Line {line_num}:\n")
batch_file.write(f"msgid: \"{msgid}\"\n")
batch_file.write(f"msgstr: \"\"\n")
batch_file.write(f"\nArabic Translation: \n")
batch_file.write(f"msgstr: \"\"\n")
batch_file.write("-" * 40 + "\n\n")
print(f"Created {batch_filename} with {len(batch_translations)} translations")
def create_summary_report(empty_translations: List[Tuple[int, str, str]]):
"""
Create a summary report of all empty translations.
"""
with open("empty_translations_summary.txt", 'w', encoding='utf-8') as report:
report.write("EMPTY TRANSLATIONS SUMMARY REPORT\n")
report.write("=" * 50 + "\n\n")
report.write(f"Total empty translations: {len(empty_translations)}\n\n")
# Group by type/pattern for better organization
ui_elements = []
form_fields = []
messages = []
navigation = []
other = []
for line_num, msgid, context in empty_translations:
msgid_lower = msgid.lower()
if any(word in msgid_lower for word in ['button', 'btn', 'click', 'select']):
ui_elements.append((line_num, msgid))
elif any(word in msgid_lower for word in ['field', 'form', 'input', 'enter']):
form_fields.append((line_num, msgid))
elif any(word in msgid_lower for word in ['error', 'success', 'warning', 'message']):
messages.append((line_num, msgid))
elif any(word in msgid_lower for word in ['menu', 'nav', 'page', 'dashboard']):
navigation.append((line_num, msgid))
else:
other.append((line_num, msgid))
report.write(f"UI Elements (Buttons, Links): {len(ui_elements)}\n")
report.write(f"Form Fields & Inputs: {len(form_fields)}\n")
report.write(f"Messages (Error/Success/Warning): {len(messages)}\n")
report.write(f"Navigation & Pages: {len(navigation)}\n")
report.write(f"Other: {len(other)}\n\n")
report.write("SAMPLE ENTRIES:\n")
report.write("-" * 30 + "\n")
for category, name, sample_count in [
(ui_elements, "UI Elements", 5),
(form_fields, "Form Fields", 5),
(messages, "Messages", 5),
(navigation, "Navigation", 5),
(other, "Other", 5)
]:
if category:
report.write(f"\n{name} (showing first {min(len(category), sample_count)}):\n")
for line_num, msgid in category[:sample_count]:
report.write(f" Line {line_num}: \"{msgid}\"\n")
def main():
"""Main function to process the django.po file."""
po_file_path = "locale/ar/LC_MESSAGES/django.po"
if not os.path.exists(po_file_path):
print(f"Error: {po_file_path} not found!")
return
print("Scanning for empty translations...")
empty_translations = find_empty_translations(po_file_path)
if not empty_translations:
print("No empty translations found! All msgstr entries have translations.")
return
# Create batch files
create_batch_files(empty_translations, batch_size=25)
# Create summary report
create_summary_report(empty_translations)
print(f"\nProcess completed!")
print(f"Check the generated batch files: translation_batch_*.txt")
print(f"Summary report: empty_translations_summary.txt")
print(f"\nNext steps:")
print(f"1. Work through each batch file systematically")
print(f"2. Add Arabic translations to each empty msgstr")
print(f"3. Update the main django.po file with completed translations")
if __name__ == "__main__":
main()

91
fix_po_duplicates.py Normal file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""
Fix duplicate message definitions in .po files
"""
import re
import os
def fix_po_duplicates(po_file):
"""Remove duplicate message definitions from a .po file"""
print(f"Processing {po_file}...")
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
# Split into entries
entries = []
current_entry = []
lines = content.split('\n')
i = 0
while i < len(lines):
line = lines[i]
if line.startswith('msgid '):
# Start of new entry
if current_entry:
entries.append('\n'.join(current_entry))
current_entry = [line]
elif line.startswith('msgstr ') and current_entry:
# End of entry
current_entry.append(line)
entries.append('\n'.join(current_entry))
current_entry = []
else:
if current_entry:
current_entry.append(line)
i += 1
# Add the last entry if it exists
if current_entry:
entries.append('\n'.join(current_entry))
# Remove duplicates, keeping the first occurrence
seen_msgids = set()
unique_entries = []
for entry in entries:
# Extract msgid
msgid_match = re.search(r'msgid\s+"([^"]*)"', entry)
if msgid_match:
msgid = msgid_match.group(1)
if msgid not in seen_msgids:
seen_msgids.add(msgid)
unique_entries.append(entry)
else:
print(f" Removing duplicate: {msgid}")
else:
# Header or other entry, keep it
unique_entries.append(entry)
# Rebuild content
new_content = '\n\n'.join(unique_entries)
# Write back to file
with open(po_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f" Fixed {po_file} - removed {len(entries) - len(unique_entries)} duplicates")
def main():
"""Fix duplicates in both English and Arabic .po files"""
po_files = [
'locale/ar/LC_MESSAGES/django.po',
'locale/en/LC_MESSAGES/django.po'
]
for po_file in po_files:
if os.path.exists(po_file):
fix_po_duplicates(po_file)
else:
print(f"File not found: {po_file}")
print("\n✅ Duplicate fixing completed!")
print("Now run: python manage.py compilemessages")
if __name__ == "__main__":
main()

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

View File

@ -3,12 +3,14 @@ from django.utils.html import format_html
from django.urls import reverse
from django.utils import timezone
from .models import (
JobPosting, Candidate, TrainingMaterial, ZoomMeeting,
JobPosting, Application, TrainingMaterial, ZoomMeeting,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,Profile,JobPostingImage,MeetingComment,
AgencyAccessLink, AgencyJobAssignment
)
from django.contrib.auth import get_user_model
User = get_user_model()
class FormFieldInline(admin.TabularInline):
model = FormField
extra = 1
@ -82,17 +84,10 @@ class HiringAgencyAdmin(admin.ModelAdmin):
readonly_fields = ['slug', 'created_at', 'updated_at']
fieldsets = (
('Basic Information', {
'fields': ('name', 'slug', 'contact_person', 'email', 'phone', 'website')
}),
('Location Details', {
'fields': ('country', 'city', 'address')
}),
('Additional Information', {
'fields': ('description', 'created_at', 'updated_at')
'fields': ('name','contact_person', 'email', 'phone', 'website','user')
}),
)
save_on_top = True
prepopulated_fields = {'slug': ('name',)}
@admin.register(JobPosting)
@ -143,43 +138,6 @@ class JobPostingAdmin(admin.ModelAdmin):
mark_as_closed.short_description = 'Mark selected jobs as closed'
@admin.register(Candidate)
class CandidateAdmin(admin.ModelAdmin):
list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied','is_resume_parsed', 'created_at']
list_filter = ['stage', 'applied', 'created_at', 'job__department']
search_fields = ['first_name', 'last_name', 'email', 'phone']
readonly_fields = ['slug', 'created_at', 'updated_at']
fieldsets = (
('Personal Information', {
'fields': ('first_name', 'last_name', 'email', 'phone', 'resume')
}),
('Application Details', {
'fields': ('job', 'applied', 'stage','is_resume_parsed')
}),
('Interview Process', {
'fields': ('exam_date', 'exam_status', 'interview_date', 'interview_status', 'offer_date', 'offer_status', 'join_date')
}),
('Scoring', {
'fields': ('ai_analysis_data',)
}),
('Additional Information', {
'fields': ('submitted_by_agency', 'created_at', 'updated_at')
}),
)
save_on_top = True
actions = ['mark_as_applied', 'mark_as_not_applied']
def mark_as_applied(self, request, queryset):
updated = queryset.update(applied=True)
self.message_user(request, f'{updated} candidates marked as applied.')
mark_as_applied.short_description = 'Mark selected candidates as applied'
def mark_as_not_applied(self, request, queryset):
updated = queryset.update(applied=False)
self.message_user(request, f'{updated} candidates marked as not applied.')
mark_as_not_applied.short_description = 'Mark selected candidates as not applied'
@admin.register(TrainingMaterial)
class TrainingMaterialAdmin(admin.ModelAdmin):
list_display = ['title', 'created_by', 'created_at']
@ -280,6 +238,7 @@ class FormSubmissionAdmin(admin.ModelAdmin):
# Register other models
admin.site.register(FormStage)
admin.site.register(Application)
admin.site.register(FormField)
admin.site.register(FieldResponse)
admin.site.register(InterviewSchedule)
@ -290,3 +249,4 @@ admin.site.register(AgencyJobAssignment)
admin.site.register(JobPostingImage)
admin.site.register(User)

View File

@ -1,17 +1,163 @@
from functools import wraps
from datetime import date
from django.shortcuts import redirect, get_object_or_404
from django.http import HttpResponseNotFound
from django.http import HttpResponseNotFound, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import PermissionDenied
from django.contrib import messages
def job_not_expired(view_func):
@wraps(view_func)
def _wrapped_view(request, job_id, *args, **kwargs):
from .models import JobPosting
job = get_object_or_404(JobPosting, pk=job_id)
if job.expiration_date and job.application_deadline< date.today():
return redirect('expired_job_page')
return view_func(request, job_id, *args, **kwargs)
return _wrapped_view
return _wrapped_view
def user_type_required(allowed_types=None, login_url=None):
"""
Decorator to restrict view access based on user type.
Args:
allowed_types (list): List of allowed user types ['staff', 'agency', 'candidate']
login_url (str): URL to redirect to if user is not authenticated
"""
if allowed_types is None:
allowed_types = ['staff']
def decorator(view_func):
@wraps(view_func)
@login_required(login_url=login_url)
def _wrapped_view(request, *args, **kwargs):
user = request.user
# Check if user has user_type attribute
if not hasattr(user, 'user_type') or not user.user_type:
messages.error(request, "User type not specified. Please contact administrator.")
return redirect('portal_login')
# Check if user type is allowed
if user.user_type not in allowed_types:
# Log unauthorized access attempt
messages.error(
request,
f"Access denied. This page is restricted to {', '.join(allowed_types)} users."
)
# Redirect based on user type
if user.user_type == 'agency':
return redirect('agency_portal_dashboard')
elif user.user_type == 'candidate':
return redirect('candidate_portal_dashboard')
else:
return redirect('dashboard')
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
class UserTypeRequiredMixin(AccessMixin):
"""
Mixin for class-based views to restrict access based on user type.
"""
allowed_user_types = ['staff'] # Default to staff only
login_url = '/login/'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
# Check if user has user_type attribute
if not hasattr(request.user, 'user_type') or not request.user.user_type:
messages.error(request, "User type not specified. Please contact administrator.")
return redirect('portal_login')
# Check if user type is allowed
if request.user.user_type not in self.allowed_user_types:
# Log unauthorized access attempt
messages.error(
request,
f"Access denied. This page is restricted to {', '.join(self.allowed_user_types)} users."
)
# Redirect based on user type
if request.user.user_type == 'agency':
return redirect('agency_portal_dashboard')
elif request.user.user_type == 'candidate':
return redirect('candidate_portal_dashboard')
else:
return redirect('dashboard')
return super().dispatch(request, *args, **kwargs)
def handle_no_permission(self):
if self.request.user.is_authenticated:
# User is authenticated but doesn't have permission
messages.error(
self.request,
f"Access denied. This page is restricted to {', '.join(self.allowed_user_types)} users."
)
return redirect('dashboard')
else:
# User is not authenticated
return super().handle_no_permission()
class StaffRequiredMixin(UserTypeRequiredMixin):
"""Mixin to restrict access to staff users only."""
allowed_user_types = ['staff']
class AgencyRequiredMixin(UserTypeRequiredMixin):
"""Mixin to restrict access to agency users only."""
allowed_user_types = ['agency']
login_url = '/portal/login/'
class CandidateRequiredMixin(UserTypeRequiredMixin):
"""Mixin to restrict access to candidate users only."""
allowed_user_types = ['candidate']
login_url = '/portal/login/'
class StaffOrAgencyRequiredMixin(UserTypeRequiredMixin):
"""Mixin to restrict access to staff and agency users."""
allowed_user_types = ['staff', 'agency']
class StaffOrCandidateRequiredMixin(UserTypeRequiredMixin):
"""Mixin to restrict access to staff and candidate users."""
allowed_user_types = ['staff', 'candidate']
def agency_user_required(view_func):
"""Decorator to restrict view to agency users only."""
return user_type_required(['agency'], login_url='/portal/login/')(view_func)
def candidate_user_required(view_func):
"""Decorator to restrict view to candidate users only."""
return user_type_required(['candidate'], login_url='/portal/login/')(view_func)
def staff_user_required(view_func):
"""Decorator to restrict view to staff users only."""
return user_type_required(['staff'])(view_func)
def staff_or_agency_required(view_func):
"""Decorator to restrict view to staff and agency users."""
return user_type_required(['staff', 'agency'], login_url='/portal/login/')(view_func)
def staff_or_candidate_required(view_func):
"""Decorator to restrict view to staff and candidate users."""
return user_type_required(['staff', 'candidate'], login_url='/portal/login/')(view_func)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
import os
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from gpt_po_translator.main import translate_po_files
class Command(BaseCommand):
help = 'Translates PO files using gpt-po-translator configured with OpenRouter.'
def add_arguments(self, parser):
parser.add_argument(
'--folder',
type=str,
default=getattr(settings, 'LOCALE_PATHS', ['locale'])[0],
help='Path to the folder containing .po files (default is the first LOCALE_PATHS entry).',
)
parser.add_argument(
'--lang',
type=str,
help='Comma-separated target language codes (e.g., de,fr,es).',
required=True,
)
parser.add_argument(
'--model',
type=str,
default='mistralai/mistral-nemo', # Example OpenRouter model
help='The OpenRouter model to use (e.g., openai/gpt-4o, mistralai/mistral-nemo).',
)
parser.add_argument(
'--bulk',
action='store_true',
help='Enable bulk translation mode for efficiency.',
)
parser.add_argument(
'--bulksize',
type=int,
default=50,
help='Entries per batch in bulk mode (default: 50).',
)
def handle(self, *args, **options):
# --- OpenRouter Configuration ---
# 1. Get API Key from environment variable
api_key = os.environ.get('OPENROUTER_API_KEY')
if not api_key:
raise CommandError("The OPENROUTER_API_KEY environment variable is not set.")
# 2. Set the base URL for OpenRouter
openrouter_base_url = "https://openrouter.ai/api/v1"
# 3. Call the core translation function, passing OpenRouter specific config
try:
self.stdout.write(self.style.NOTICE(f"Starting translation with model: {options['model']} via OpenRouter..."))
translate_po_files(
folder=options['folder'],
lang_codes=options['lang'].split(','),
provider='openai', # gpt-po-translator uses 'openai' provider for OpenAI-compatible APIs
api_key=api_key,
model_name=options['model'],
bulk=options['bulk'],
bulk_size=options['bulksize'],
# Set the base_url for the OpenAI client to point to OpenRouter
base_url=openrouter_base_url,
# OpenRouter often requires a referrer for API usage
extra_headers={"HTTP-Referer": "http://your-django-app.com"},
)
self.stdout.write(self.style.SUCCESS(f"Successfully translated PO files for languages: {options['lang']}"))
except Exception as e:
raise CommandError(f"An error occurred during translation: {e}")

View File

@ -1,7 +1,10 @@
# Generated by Django 5.2.7 on 2025-11-05 13:05
# Generated by Django 5.2.6 on 2025-11-10 14:13
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import django_ckeditor_5.fields
import django_countries.fields
import django_extensions.db.fields
@ -15,7 +18,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
@ -44,28 +47,6 @@ class Migration(migrations.Migration):
'ordering': ['order'],
},
),
migrations.CreateModel(
name='HiringAgency',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
('address', models.TextField(blank=True, null=True)),
],
options={
'verbose_name': 'Hiring Agency',
'verbose_name_plural': 'Hiring Agencies',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Participants',
fields=[
@ -137,6 +118,78 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Application',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
('cover_letter', models.FileField(blank=True, null=True, upload_to='cover_letters/', verbose_name='Cover Letter')),
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
('applied', models.BooleanField(default=False, verbose_name='Applied')),
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer'), ('Hired', 'Hired'), ('Rejected', 'Rejected')], db_index=True, default='Applied', max_length=20, verbose_name='Stage')),
('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=20, null=True, verbose_name='Applicant Status')),
('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')),
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Exam Status')),
('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')),
('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Interview Status')),
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected'), ('Pending', 'Pending')], max_length=20, null=True, verbose_name='Offer Status')),
('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')),
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
('ai_analysis_data', models.JSONField(blank=True, default=dict, help_text='Full JSON output from the resume scoring model.', null=True, verbose_name='AI Analysis Data')),
('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')),
('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='application_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
],
options={
'verbose_name': 'Application',
'verbose_name_plural': 'Applications',
},
),
migrations.CreateModel(
name='Candidate',
fields=[
],
options={
'verbose_name': 'Candidate (Legacy)',
'verbose_name_plural': 'Candidates (Legacy)',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('recruitment.application',),
),
migrations.CreateModel(
name='FormField',
fields=[
@ -206,42 +259,33 @@ class Migration(migrations.Migration):
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
),
migrations.CreateModel(
name='Candidate',
name='HiringAgency',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('email', models.EmailField(db_index=True, max_length=254, verbose_name='Email')),
('phone', models.CharField(max_length=20, verbose_name='Phone')),
('address', models.TextField(max_length=200, verbose_name='Address')),
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')),
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
('applied', models.BooleanField(default=False, verbose_name='Applied')),
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer'), ('Hired', 'Hired')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')),
('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')),
('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')),
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')),
('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')),
('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')),
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')),
('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')),
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')),
('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')),
('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')),
('hiring_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
('address', models.TextField(blank=True, null=True)),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agency_profile', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Candidate',
'verbose_name_plural': 'Candidates',
'verbose_name': 'Hiring Agency',
'verbose_name_plural': 'Hiring Agencies',
'ordering': ['name'],
},
),
migrations.AddField(
model_name='application',
name='hiring_agency',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='recruitment.hiringagency', verbose_name='Hiring Agency'),
),
migrations.CreateModel(
name='JobPosting',
fields=[
@ -251,8 +295,8 @@ class Migration(migrations.Migration):
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=200)),
('department', models.CharField(blank=True, max_length=100)),
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
('job_type', models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='FULL_TIME', max_length=20)),
('workplace_type', models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='ON_SITE', max_length=20)),
('location_city', models.CharField(blank=True, max_length=100)),
('location_state', models.CharField(blank=True, max_length=100)),
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
@ -281,6 +325,7 @@ class Migration(migrations.Migration):
('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')),
('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')),
('cancelled_at', models.DateTimeField(blank=True, null=True)),
('assigned_to', models.ForeignKey(blank=True, help_text='The user who has been assigned to this job', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Assigned To')),
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('users', models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant')),
('participants', models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant')),
@ -308,7 +353,7 @@ class Migration(migrations.Migration):
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
('candidates', models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate')),
('candidates', models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.application')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
],
@ -319,9 +364,9 @@ class Migration(migrations.Migration):
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
),
migrations.AddField(
model_name='candidate',
model_name='application',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.jobposting', verbose_name='Job'),
),
migrations.CreateModel(
name='AgencyJobAssignment',
@ -356,6 +401,77 @@ class Migration(migrations.Migration):
('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('subject', models.CharField(max_length=200, verbose_name='Subject')),
('content', models.TextField(verbose_name='Message Content')),
('message_type', models.CharField(choices=[('direct', 'Direct Message'), ('job_related', 'Job Related'), ('system', 'System Notification')], default='direct', max_length=20, verbose_name='Message Type')),
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')),
('job', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.jobposting', verbose_name='Related Job')),
('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')),
],
options={
'verbose_name': 'Message',
'verbose_name_plural': 'Messages',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Person',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')),
('email', models.EmailField(db_index=True, help_text='Unique email address for the person', max_length=254, unique=True, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other'), ('P', 'Prefer not to say')], max_length=1, null=True, verbose_name='Gender')),
('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')),
('address', models.TextField(blank=True, null=True, verbose_name='Address')),
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
],
options={
'verbose_name': 'Person',
'verbose_name_plural': 'People',
},
),
migrations.CreateModel(
name='Document',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('file', models.FileField(upload_to='candidate_documents/%Y/%m/', validators=[recruitment.validators.validate_image_size], verbose_name='Document File')),
('document_type', models.CharField(choices=[('resume', 'Resume'), ('cover_letter', 'Cover Letter'), ('certificate', 'Certificate'), ('id_document', 'ID Document'), ('passport', 'Passport'), ('education', 'Education Document'), ('experience', 'Experience Letter'), ('other', 'Other')], default='other', max_length=20, verbose_name='Document Type')),
('description', models.CharField(blank=True, max_length=200, verbose_name='Description')),
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Uploaded By')),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='recruitment.person', verbose_name='Person')),
],
options={
'verbose_name': 'Document',
'verbose_name_plural': 'Documents',
'ordering': ['-created_at'],
},
),
migrations.AddField(
model_name='application',
name='person',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.person', verbose_name='Person'),
),
migrations.CreateModel(
name='Profile',
fields=[
@ -435,7 +551,7 @@ class Migration(migrations.Migration):
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.application')),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
@ -543,14 +659,6 @@ class Migration(migrations.Migration):
model_name='formtemplate',
index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'),
),
migrations.AddIndex(
model_name='candidate',
index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'),
),
migrations.AddIndex(
model_name='candidate',
index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'),
),
migrations.AddIndex(
model_name='agencyjobassignment',
index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'),
@ -571,6 +679,58 @@ class Migration(migrations.Migration):
name='agencyjobassignment',
unique_together={('agency', 'job')},
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['sender', 'created_at'], name='recruitment_sender__49d984_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['recipient', 'is_read', 'created_at'], name='recruitment_recipie_af0e6d_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['job', 'created_at'], name='recruitment_job_id_18f813_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'),
),
migrations.AddIndex(
model_name='person',
index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'),
),
migrations.AddIndex(
model_name='person',
index=models.Index(fields=['first_name', 'last_name'], name='recruitment_first_n_739de5_idx'),
),
migrations.AddIndex(
model_name='person',
index=models.Index(fields=['created_at'], name='recruitment_created_33495a_idx'),
),
migrations.AddIndex(
model_name='document',
index=models.Index(fields=['person', 'document_type', 'created_at'], name='recruitment_person__0a6844_idx'),
),
migrations.AddIndex(
model_name='application',
index=models.Index(fields=['person', 'job'], name='recruitment_person__34355c_idx'),
),
migrations.AddIndex(
model_name='application',
index=models.Index(fields=['stage'], name='recruitment_stage_52c2d1_idx'),
),
migrations.AddIndex(
model_name='application',
index=models.Index(fields=['created_at'], name='recruitment_created_80633f_idx'),
),
migrations.AddIndex(
model_name='application',
index=models.Index(fields=['person', 'stage', 'created_at'], name='recruitment_person__8715ec_idx'),
),
migrations.AlterUniqueTogether(
name='application',
unique_together={('person', 'job')},
),
migrations.AddIndex(
model_name='jobposting',
index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'),
@ -589,7 +749,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'),
index=models.Index(fields=['application', 'job'], name='recruitment_applica_927561_idx'),
),
migrations.AddIndex(
model_name='notification',

View File

@ -0,0 +1,25 @@
# Generated by Django 5.2.6 on 2025-11-11 10:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Candidate',
),
migrations.RenameField(
model_name='interviewschedule',
old_name='candidates',
new_name='applications',
),
migrations.RemoveField(
model_name='application',
name='user',
),
]

View File

@ -0,0 +1,45 @@
# Generated by Django 5.2.6 on 2025-11-11 12:13
import django.db.models.deletion
import recruitment.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('recruitment', '0002_delete_candidate_and_more'),
]
operations = [
migrations.RemoveIndex(
model_name='document',
name='recruitment_person__0a6844_idx',
),
migrations.RemoveField(
model_name='document',
name='person',
),
migrations.AddField(
model_name='document',
name='content_type',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content Type'),
preserve_default=False,
),
migrations.AddField(
model_name='document',
name='object_id',
field=models.PositiveIntegerField(default=1, verbose_name='Object ID'),
preserve_default=False,
),
migrations.AlterField(
model_name='document',
name='file',
field=models.FileField(upload_to='documents/%Y/%m/', validators=[recruitment.validators.validate_image_size], verbose_name='Document File'),
),
migrations.AddIndex(
model_name='document',
index=models.Index(fields=['content_type', 'object_id', 'document_type', 'created_at'], name='recruitment_content_547650_idx'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.6 on 2025-11-12 20:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_convert_document_to_generic_fk'),
]
operations = [
migrations.AddField(
model_name='person',
name='agency',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency'),
),
]

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,14 @@
from rest_framework import serializers
from .models import JobPosting, Candidate
from .models import JobPosting, Application
class JobPostingSerializer(serializers.ModelSerializer):
class Meta:
model = JobPosting
fields = '__all__'
class CandidateSerializer(serializers.ModelSerializer):
class ApplicationSerializer(serializers.ModelSerializer):
job_title = serializers.CharField(source='job.title', read_only=True)
class Meta:
model = Candidate
model = Application
fields = '__all__'

View File

@ -1,18 +1,20 @@
import logging
import random
from datetime import datetime, timedelta
from django.db import transaction
from django_q.models import Schedule
from django_q.tasks import schedule
from django.dispatch import receiver
from django_q.tasks import async_task
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.utils import timezone
from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting,Notification,AgencyJobAssignment,AgencyAccessLink
from .models import FormField,FormStage,FormTemplate,Application,JobPosting,Notification,HiringAgency,Person
from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__)
User = get_user_model()
@receiver(post_save, sender=JobPosting)
def format_job(sender, instance, created, **kwargs):
if created:
@ -57,9 +59,9 @@ def format_job(sender, instance, created, **kwargs):
# instance.form_template.is_active = False
# instance.save()
@receiver(post_save, sender=Candidate)
@receiver(post_save, sender=Application)
def score_candidate_resume(sender, instance, created, **kwargs):
if not instance.is_resume_parsed:
if instance.resume and not instance.is_resume_parsed:
logger.info(f"Scoring resume for candidate {instance.pk}")
async_task(
'recruitment.tasks.handle_reume_parsing_and_scoring',
@ -399,11 +401,33 @@ def notification_created(sender, instance, created, **kwargs):
logger.info(f"Notification cached for SSE: {notification_data}")
@receiver(post_save,sender=AgencyJobAssignment)
def create_access_link(sender,instance,created,**kwargs):
def generate_random_password():
import string
return ''.join(random.choices(string.ascii_letters + string.digits, k=12))
@receiver(post_save, sender=HiringAgency)
def hiring_agency_created(sender, instance, created, **kwargs):
if created:
link=AgencyAccessLink(assignment=instance)
link.access_password = link.generate_password()
link.unique_token = link.generate_token()
link.expires_at = datetime.now() + timedelta(days=4)
link.save()
logger.info(f"New hiring agency created: {instance.pk} - {instance.name}")
user = User.objects.create_user(
username=instance.name,
email=instance.email,
user_type="agency"
)
user.set_password(generate_random_password())
user.save()
instance.user = user
instance.save()
@receiver(post_save, sender=Person)
def person_created(sender, instance, created, **kwargs):
if created:
logger.info(f"New Person created: {instance.pk} - {instance.email}")
user = User.objects.create_user(
username=instance.slug,
first_name=instance.first_name,
last_name=instance.last_name,
email=instance.email,
phone=instance.phone,
user_type="candidate"
)
instance.user = user
instance.save()

View File

@ -7,7 +7,7 @@ from PyPDF2 import PdfReader
from datetime import datetime
from django.db import transaction
from .utils import create_zoom_meeting
from recruitment.models import Candidate
from recruitment.models import Application
from . linkedin_service import LinkedInService
from django.shortcuts import get_object_or_404
from . models import JobPosting
@ -244,8 +244,8 @@ def handle_reume_parsing_and_scoring(pk):
# --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) ---
try:
instance = Candidate.objects.get(pk=pk)
except Candidate.DoesNotExist:
instance = Application.objects.get(pk=pk)
except Application.DoesNotExist:
# Exit gracefully if the candidate was deleted after the task was queued
logger.warning(f"Candidate matching query does not exist for pk={pk}. Exiting task.")
print(f"Candidate matching query does not exist for pk={pk}. Exiting task.")
@ -453,7 +453,7 @@ def create_interview_and_meeting(
Synchronous task for a single interview slot, dispatched by django-q.
"""
try:
candidate = Candidate.objects.get(pk=candidate_id)
candidate = Application.objects.get(pk=candidate_id)
job = JobPosting.objects.get(pk=job_id)
schedule = InterviewSchedule.objects.get(pk=schedule_id)
@ -477,7 +477,7 @@ def create_interview_and_meeting(
password=result["meeting_details"]["password"]
)
ScheduledInterview.objects.create(
candidate=candidate,
application=Application,
job=job,
zoom_meeting=zoom_meeting,
schedule=schedule,
@ -485,11 +485,11 @@ def create_interview_and_meeting(
interview_time=slot_time
)
# Log success or use Django-Q result system for monitoring
logger.info(f"Successfully scheduled interview for {candidate.name}")
logger.info(f"Successfully scheduled interview for {Application.name}")
return True # Task succeeded
else:
# Handle Zoom API failure (e.g., log it or notify administrator)
logger.error(f"Zoom API failed for {candidate.name}: {result['message']}")
logger.error(f"Zoom API failed for {Application.name}: {result['message']}")
return False # Task failed
except Exception as e:
@ -704,14 +704,14 @@ def sync_candidate_to_source_task(candidate_id, source_id):
try:
# Get the candidate and source
candidate = Candidate.objects.get(pk=candidate_id)
application = Application.objects.get(pk=candidate_id)
source = Source.objects.get(pk=source_id)
# Initialize sync service
sync_service = CandidateSyncService()
# Perform the sync operation
result = sync_service.sync_candidate_to_source(candidate, source)
result = sync_service.sync_candidate_to_source(application, source)
# Log the operation
IntegrationLog.objects.create(
@ -719,7 +719,7 @@ def sync_candidate_to_source_task(candidate_id, source_id):
action=IntegrationLog.ActionChoices.SYNC,
endpoint=source.sync_endpoint or "unknown",
method=source.sync_method or "POST",
request_data={"candidate_id": candidate_id, "candidate_name": candidate.name},
request_data={"candidate_id": candidate_id, "application_name": application.name},
response_data=result,
status_code="SUCCESS" if result.get('success') else "ERROR",
error_message=result.get('error') if not result.get('success') else None,
@ -731,8 +731,8 @@ def sync_candidate_to_source_task(candidate_id, source_id):
logger.info(f"Sync completed for candidate {candidate_id} to source {source_id}: {result}")
return result
except Candidate.DoesNotExist:
error_msg = f"Candidate not found: {candidate_id}"
except Application.DoesNotExist:
error_msg = f"Application not found: {candidate_id}"
logger.error(error_msg)
return {"success": False, "error": error_msg}

View File

@ -0,0 +1,27 @@
from django import template
register = template.Library()
@register.filter
def filename(value):
"""
Extract just the filename from a file path.
Example: 'documents/resume.pdf' -> 'resume.pdf'
"""
if not value:
return ''
# Convert to string and split by path separators
import os
return os.path.basename(str(value))
@register.filter
def split(value, delimiter):
"""
Split a string by a delimiter and return a list.
This is a custom implementation of the split functionality.
"""
if not value:
return []
return str(value).split(delimiter)

View File

@ -1,5 +1,5 @@
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from django.core.files.uploadedfile import SimpleUploadedFile
@ -7,6 +7,8 @@ from datetime import datetime, time, timedelta
import json
from unittest.mock import patch, MagicMock
User = get_user_model()
from .models import (
JobPosting, Candidate, ZoomMeeting, FormTemplate, FormStage, FormField,
FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview,
@ -14,11 +16,11 @@ from .models import (
)
from .forms import (
JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm,
CandidateStageForm, InterviewScheduleForm
CandidateStageForm, InterviewScheduleForm, CandidateSignupForm
)
from .views import (
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, candidate_screening_view,
candidate_exam_view, candidate_interview_view, submit_form, api_schedule_candidate_meeting
candidate_exam_view, candidate_interview_view, api_schedule_candidate_meeting
)
from .views_frontend import CandidateListView, JobListView
from .utils import create_zoom_meeting, get_candidates_from_request
@ -46,14 +48,21 @@ class BaseTestCase(TestCase):
location_country='Saudi Arabia',
description='Job description',
qualifications='Job qualifications',
application_deadline=timezone.now() + timedelta(days=30),
created_by=self.user
)
self.candidate = Candidate.objects.create(
# Create a person first
from .models import Person
person = Person.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
phone='1234567890',
phone='1234567890'
)
self.candidate = Candidate.objects.create(
person=person,
resume=SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf'),
job=self.job,
stage='Applied'
@ -231,28 +240,6 @@ class ViewTests(BaseTestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'success')
def test_submit_form(self):
"""Test submit_form view"""
# Create a form template first
template = FormTemplate.objects.create(
job=self.job,
name='Test Template',
created_by=self.user,
is_active=True
)
data = {
'field_1': 'John', # Assuming field ID 1 corresponds to First Name
'field_2': 'Doe', # Assuming field ID 2 corresponds to Last Name
'field_3': 'john@example.com', # Email
}
response = self.client.post(
reverse('application_submit', kwargs={'template_id': template.id}),
data
)
# After successful submission, should redirect to success page
self.assertEqual(response.status_code, 302)
class FormTests(BaseTestCase):
@ -268,13 +255,13 @@ class FormTests(BaseTestCase):
'location_city': 'Riyadh',
'location_state': 'Riyadh',
'location_country': 'Saudi Arabia',
'description': 'Job description',
'description': 'Job description with at least 20 characters to meet validation requirements',
'qualifications': 'Job qualifications',
'salary_range': '5000-7000',
'application_deadline': '2025-12-31',
'max_applications': '100',
'open_positions': '2',
'hash_tags': '#hiring, #jobopening'
'hash_tags': '#hiring,#jobopening'
}
form = JobPostingForm(data=form_data)
self.assertTrue(form.is_valid())
@ -315,24 +302,51 @@ class FormTests(BaseTestCase):
form_data = {
'stage': 'Exam'
}
form = CandidateStageForm(data=form_data, candidate=self.candidate)
form = CandidateStageForm(data=form_data, instance=self.candidate)
self.assertTrue(form.is_valid())
def test_interview_schedule_form(self):
"""Test InterviewScheduleForm"""
# Update candidate to Interview stage first
self.candidate.stage = 'Interview'
self.candidate.save()
form_data = {
'candidates': [self.candidate.id],
'start_date': (timezone.now() + timedelta(days=1)).date(),
'end_date': (timezone.now() + timedelta(days=7)).date(),
'working_days': [0, 1, 2, 3, 4], # Monday to Friday
'start_time': '09:00',
'end_time': '17:00',
'interview_duration': 60,
'buffer_time': 15
}
form = InterviewScheduleForm(slug=self.job.slug, data=form_data)
self.assertTrue(form.is_valid())
def test_candidate_signup_form_valid(self):
"""Test CandidateSignupForm with valid data"""
form_data = {
'first_name': 'John',
'last_name': 'Doe',
'email': 'john.doe@example.com',
'phone': '+1234567890',
'password': 'SecurePass123',
'confirm_password': 'SecurePass123'
}
form = CandidateSignupForm(data=form_data)
self.assertTrue(form.is_valid())
def test_candidate_signup_form_password_mismatch(self):
"""Test CandidateSignupForm with password mismatch"""
form_data = {
'first_name': 'John',
'last_name': 'Doe',
'email': 'john.doe@example.com',
'phone': '+1234567890',
'password': 'SecurePass123',
'confirm_password': 'DifferentPass123'
}
form = CandidateSignupForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('Passwords do not match', str(form.errors))
class IntegrationTests(BaseTestCase):
"""Integration tests for multiple components"""
@ -340,11 +354,14 @@ class IntegrationTests(BaseTestCase):
def test_candidate_journey(self):
"""Test the complete candidate journey from application to interview"""
# 1. Create candidate
candidate = Candidate.objects.create(
person = Person.objects.create(
first_name='Jane',
last_name='Smith',
email='jane@example.com',
phone='9876543210',
phone='9876543210'
)
candidate = Candidate.objects.create(
person=person,
resume=SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf'),
job=self.job,
stage='Applied'
@ -449,11 +466,15 @@ class PerformanceTests(BaseTestCase):
"""Test pagination with large datasets"""
# Create many candidates
for i in range(100):
Candidate.objects.create(
person = Person.objects.create(
first_name=f'Candidate{i}',
last_name=f'Test{i}',
email=f'candidate{i}@example.com',
phone=f'123456789{i}',
phone=f'123456789{i}'
)
Candidate.objects.create(
person=person,
resume=SimpleUploadedFile(f'resume{i}.pdf', b'file_content', content_type='application/pdf'),
job=self.job,
stage='Applied'
)
@ -594,13 +615,17 @@ class TestFactories:
@staticmethod
def create_candidate(**kwargs):
job = TestFactories.create_job_posting()
person = Person.objects.create(
first_name='Test',
last_name='Candidate',
email='test@example.com',
phone='1234567890'
)
defaults = {
'first_name': 'Test',
'last_name': 'Candidate',
'email': 'test@example.com',
'phone': '1234567890',
'person': person,
'job': job,
'stage': 'Applied'
'stage': 'Applied',
'resume': SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf')
}
defaults.update(kwargs)
return Candidate.objects.create(**defaults)

View File

@ -5,13 +5,22 @@ from . import views_integration
from . import views_source
urlpatterns = [
path('', views_frontend.dashboard_view, name='dashboard'),
path("", views_frontend.dashboard_view, name="dashboard"),
# Job URLs (using JobPosting model)
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
path('jobs/create/', views.create_job, name='job_create'),
path('job/<slug:slug>/upload_image_simple/', views.job_image_upload, name='job_image_upload'),
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
path("persons/", views.PersonListView.as_view(), name="person_list"),
path("persons/create/", views.PersonCreateView.as_view(), name="person_create"),
path("persons/<slug:slug>/", views.PersonDetailView.as_view(), name="person_detail"),
path("persons/<slug:slug>/update/", views.PersonUpdateView.as_view(), name="person_update"),
path("persons/<slug:slug>/delete/", views.PersonDeleteView.as_view(), name="person_delete"),
path("jobs/", views_frontend.JobListView.as_view(), name="job_list"),
path("jobs/create/", views.create_job, name="job_create"),
path(
"job/<slug:slug>/upload_image_simple/",
views.job_image_upload,
name="job_image_upload",
),
path("jobs/<slug:slug>/update/", views.edit_job, name="job_update"),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
path('jobs/<slug:slug>/download/cvs/', views.job_cvs_download, name='job_cvs_download'),
@ -19,85 +28,250 @@ urlpatterns = [
path('careers/',views.kaauh_career,name='kaauh_career'),
# LinkedIn Integration URLs
path('jobs/<slug:slug>/post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'),
path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'),
path('jobs/linkedin/callback/', views.linkedin_callback, name='linkedin_callback'),
path('jobs/<slug:slug>/schedule-interviews/', views.schedule_interviews_view, name='schedule_interviews'),
path('jobs/<slug:slug>/confirm-schedule-interviews/', views.confirm_schedule_interviews_view, name='confirm_schedule_interviews_view'),
path(
"jobs/<slug:slug>/post-to-linkedin/",
views.post_to_linkedin,
name="post_to_linkedin",
),
path("jobs/linkedin/login/", views.linkedin_login, name="linkedin_login"),
path("jobs/linkedin/callback/", views.linkedin_callback, name="linkedin_callback"),
path(
"jobs/<slug:slug>/schedule-interviews/",
views.schedule_interviews_view,
name="schedule_interviews",
),
path(
"jobs/<slug:slug>/confirm-schedule-interviews/",
views.confirm_schedule_interviews_view,
name="confirm_schedule_interviews_view",
),
# Candidate URLs
path('candidates/', views_frontend.CandidateListView.as_view(), name='candidate_list'),
path('candidates/create/', views_frontend.CandidateCreateView.as_view(), name='candidate_create'),
path('candidates/create/<slug:slug>/', views_frontend.CandidateCreateView.as_view(), name='candidate_create_for_job'),
path('jobs/<slug:slug>/candidates/', views_frontend.JobCandidatesListView.as_view(), name='job_candidates_list'),
path('candidates/<slug:slug>/update/', views_frontend.CandidateUpdateView.as_view(), name='candidate_update'),
path('candidates/<slug:slug>/delete/', views_frontend.CandidateDeleteView.as_view(), name='candidate_delete'),
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
path('candidate/<slug:slug>/resume-template/', views_frontend.candidate_resume_template_view, name='candidate_resume_template'),
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
path('candidate/<slug:slug>/retry-scoring/', views_frontend.retry_scoring_view, name='candidate_retry_scoring'),
path(
"candidates/", views_frontend.ApplicationListView.as_view(), name="candidate_list"
),
path(
"candidates/create/",
views_frontend.ApplicationCreateView.as_view(),
name="candidate_create",
),
path(
"candidates/create/<slug:slug>/",
views_frontend.ApplicationCreateView.as_view(),
name="candidate_create_for_job",
),
path(
"jobs/<slug:slug>/candidates/",
views_frontend.JobApplicationListView.as_view(),
name="job_candidates_list",
),
path(
"candidates/<slug:slug>/update/",
views_frontend.ApplicationUpdateView.as_view(),
name="candidate_update",
),
path(
"candidates/<slug:slug>/delete/",
views_frontend.ApplicationDeleteView.as_view(),
name="candidate_delete",
),
path(
"candidate/<slug:slug>/view/",
views_frontend.candidate_detail,
name="candidate_detail",
),
path(
"candidate/<slug:slug>/resume-template/",
views_frontend.candidate_resume_template_view,
name="candidate_resume_template",
),
path(
"candidate/<slug:slug>/update-stage/",
views_frontend.candidate_update_stage,
name="candidate_update_stage",
),
path(
"candidate/<slug:slug>/retry-scoring/",
views_frontend.retry_scoring_view,
name="candidate_retry_scoring",
),
# Training URLs
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
path('training/create/', views_frontend.TrainingCreateView.as_view(), name='training_create'),
path('training/<slug:slug>/', views_frontend.TrainingDetailView.as_view(), name='training_detail'),
path('training/<slug:slug>/update/', views_frontend.TrainingUpdateView.as_view(), name='training_update'),
path('training/<slug:slug>/delete/', views_frontend.TrainingDeleteView.as_view(), name='training_delete'),
path("training/", views_frontend.TrainingListView.as_view(), name="training_list"),
path(
"training/create/",
views_frontend.TrainingCreateView.as_view(),
name="training_create",
),
path(
"training/<slug:slug>/",
views_frontend.TrainingDetailView.as_view(),
name="training_detail",
),
path(
"training/<slug:slug>/update/",
views_frontend.TrainingUpdateView.as_view(),
name="training_update",
),
path(
"training/<slug:slug>/delete/",
views_frontend.TrainingDeleteView.as_view(),
name="training_delete",
),
# Meeting URLs
path('meetings/', views.ZoomMeetingListView.as_view(), name='list_meetings'),
path('meetings/create-meeting/', views.ZoomMeetingCreateView.as_view(), name='create_meeting'),
path('meetings/meeting-details/<slug:slug>/', views.ZoomMeetingDetailsView.as_view(), name='meeting_details'),
path('meetings/update-meeting/<slug:slug>/', views.ZoomMeetingUpdateView.as_view(), name='update_meeting'),
path('meetings/delete-meeting/<slug:slug>/', views.ZoomMeetingDeleteView, name='delete_meeting'),
path("meetings/", views.ZoomMeetingListView.as_view(), name="list_meetings"),
path(
"meetings/create-meeting/",
views.ZoomMeetingCreateView.as_view(),
name="create_meeting",
),
path(
"meetings/meeting-details/<slug:slug>/",
views.ZoomMeetingDetailsView.as_view(),
name="meeting_details",
),
path(
"meetings/update-meeting/<slug:slug>/",
views.ZoomMeetingUpdateView.as_view(),
name="update_meeting",
),
path(
"meetings/delete-meeting/<slug:slug>/",
views.ZoomMeetingDeleteView,
name="delete_meeting",
),
# JobPosting functional views URLs (keeping for compatibility)
path('api/create/', views.create_job, name='create_job_api'),
path('api/<slug:slug>/edit/', views.edit_job, name='edit_job_api'),
path("api/create/", views.create_job, name="create_job_api"),
path("api/<slug:slug>/edit/", views.edit_job, name="edit_job_api"),
# ERP Integration URLs
path('integration/erp/', views_integration.ERPIntegrationView.as_view(), name='erp_integration'),
path('integration/erp/create-job/', views_integration.erp_create_job_view, name='erp_create_job'),
path('integration/erp/update-job/', views_integration.erp_update_job_view, name='erp_update_job'),
path('integration/erp/health/', views_integration.erp_integration_health, name='erp_integration_health'),
path(
"integration/erp/",
views_integration.ERPIntegrationView.as_view(),
name="erp_integration",
),
path(
"integration/erp/create-job/",
views_integration.erp_create_job_view,
name="erp_create_job",
),
path(
"integration/erp/update-job/",
views_integration.erp_update_job_view,
name="erp_update_job",
),
path(
"integration/erp/health/",
views_integration.erp_integration_health,
name="erp_integration_health",
),
# Form Preview URLs
# path('forms/', views.form_list, name='form_list'),
path('forms/builder/', views.form_builder, name='form_builder'),
path('forms/builder/<slug:template_slug>/', views.form_builder, name='form_builder'),
path('forms/', views.form_templates_list, name='form_templates_list'),
path('forms/create-template/', views.create_form_template, name='create_form_template'),
path('jobs/<slug:slug>/edit_linkedin_post_content/',views.edit_linkedin_post_content,name='edit_linkedin_post_content'),
path('jobs/<slug:slug>/candidate_screening_view/', views.candidate_screening_view, name='candidate_screening_view'),
path('jobs/<slug:slug>/candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'),
path('jobs/<slug:slug>/candidate_interview_view/', views.candidate_interview_view, name='candidate_interview_view'),
path('jobs/<slug:slug>/candidate_offer_view/', views_frontend.candidate_offer_view, name='candidate_offer_view'),
path('jobs/<slug:slug>/candidate_hired_view/', views_frontend.candidate_hired_view, name='candidate_hired_view'),
path('jobs/<slug:job_slug>/export/<str:stage>/csv/', views_frontend.export_candidates_csv, name='export_candidates_csv'),
path('jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/update_status/<str:stage_type>/<str:status>/', views_frontend.update_candidate_status, name='update_candidate_status'),
path("forms/builder/", views.form_builder, name="form_builder"),
path(
"forms/builder/<slug:template_slug>/", views.form_builder, name="form_builder"
),
path("forms/", views.form_templates_list, name="form_templates_list"),
path(
"forms/create-template/",
views.create_form_template,
name="create_form_template",
),
path(
"jobs/<slug:slug>/edit_linkedin_post_content/",
views.edit_linkedin_post_content,
name="edit_linkedin_post_content",
),
path(
"jobs/<slug:slug>/candidate_screening_view/",
views.candidate_screening_view,
name="candidate_screening_view",
),
path(
"jobs/<slug:slug>/candidate_exam_view/",
views.candidate_exam_view,
name="candidate_exam_view",
),
path(
"jobs/<slug:slug>/candidate_interview_view/",
views.candidate_interview_view,
name="candidate_interview_view",
),
path(
"jobs/<slug:slug>/candidate_offer_view/",
views_frontend.candidate_offer_view,
name="candidate_offer_view",
),
path(
"jobs/<slug:slug>/candidate_hired_view/",
views_frontend.candidate_hired_view,
name="candidate_hired_view",
),
path(
"jobs/<slug:job_slug>/export/<str:stage>/csv/",
views_frontend.export_candidates_csv,
name="export_candidates_csv",
),
path(
"jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/update_status/<str:stage_type>/<str:status>/",
views_frontend.update_candidate_status,
name="update_candidate_status",
),
# Sync URLs
path('jobs/<slug:job_slug>/sync-hired-candidates/', views_frontend.sync_hired_candidates, name='sync_hired_candidates'),
path('sources/<int:source_id>/test-connection/', views_frontend.test_source_connection, name='test_source_connection'),
path('jobs/<slug:slug>/<int:candidate_id>/reschedule_meeting_for_candidate/<int:meeting_id>/', views.reschedule_meeting_for_candidate, name='reschedule_meeting_for_candidate'),
path('jobs/<slug:slug>/update_candidate_exam_status/', views.update_candidate_exam_status, name='update_candidate_exam_status'),
path('jobs/<slug:slug>/bulk_update_candidate_exam_status/', views.bulk_update_candidate_exam_status, name='bulk_update_candidate_exam_status'),
path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'),
path('htmx/<slug:slug>/candidate_set_exam_date/', views.candidate_set_exam_date, name='candidate_set_exam_date'),
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
path(
"jobs/<slug:job_slug>/sync-hired-candidates/",
views_frontend.sync_hired_candidates,
name="sync_hired_candidates",
),
path(
"sources/<int:source_id>/test-connection/",
views_frontend.test_source_connection,
name="test_source_connection",
),
path(
"jobs/<slug:slug>/<int:candidate_id>/reschedule_meeting_for_candidate/<int:meeting_id>/",
views.reschedule_meeting_for_candidate,
name="reschedule_meeting_for_candidate",
),
path(
"jobs/<slug:slug>/update_candidate_exam_status/",
views.update_candidate_exam_status,
name="update_candidate_exam_status",
),
path(
"jobs/<slug:slug>/bulk_update_candidate_exam_status/",
views.bulk_update_candidate_exam_status,
name="bulk_update_candidate_exam_status",
),
path(
"htmx/<int:pk>/candidate_criteria_view/",
views.candidate_criteria_view_htmx,
name="candidate_criteria_view_htmx",
),
path(
"htmx/<slug:slug>/candidate_set_exam_date/",
views.candidate_set_exam_date,
name="candidate_set_exam_date",
),
path(
"htmx/<slug:slug>/candidate_update_status/",
views.candidate_update_status,
name="candidate_update_status",
),
# path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
# path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
path('forms/<int:template_id>/submissions/<slug:slug>/', views.form_submission_details, name='form_submission_details'),
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
path(
"forms/<int:template_id>/submissions/<slug:slug>/",
views.form_submission_details,
name="form_submission_details",
),
path(
"forms/template/<slug:slug>/submissions/",
views.form_template_submissions_list,
name="form_template_submissions_list",
),
path(
"forms/template/<int:template_id>/all-submissions/",
views.form_template_all_submissions,
name="form_template_all_submissions",
),
# path('forms/<int:form_id>/', views.form_preview, name='form_preview'),
# path('forms/<int:form_id>/submit/', views.form_submit, name='form_submit'),
# path('forms/<int:form_id>/embed/', views.form_embed, name='form_embed'),
@ -110,74 +284,188 @@ urlpatterns = [
# path('api/templates/save/', views.save_form_template, name='save_form_template'),
# path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
# path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
path('jobs/<slug:slug>/calendar/', views.interview_calendar_view, name='interview_calendar'),
path('jobs/<slug:slug>/calendar/interview/<int:interview_id>/', views.interview_detail_view, name='interview_detail'),
path(
"jobs/<slug:slug>/calendar/",
views.interview_calendar_view,
name="interview_calendar",
),
path(
"jobs/<slug:slug>/calendar/interview/<int:interview_id>/",
views.interview_detail_view,
name="interview_detail",
),
# Candidate Meeting Scheduling/Rescheduling URLs
path('jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/', views.schedule_candidate_meeting, name='schedule_candidate_meeting'),
path('api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/', views.api_schedule_candidate_meeting, name='api_schedule_candidate_meeting'),
path('jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/', views.reschedule_candidate_meeting, name='reschedule_candidate_meeting'),
path('api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/', views.api_reschedule_candidate_meeting, name='api_reschedule_candidate_meeting'),
path(
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
views.schedule_candidate_meeting,
name="schedule_candidate_meeting",
),
path(
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
views.api_schedule_candidate_meeting,
name="api_schedule_candidate_meeting",
),
path(
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
views.reschedule_candidate_meeting,
name="reschedule_candidate_meeting",
),
path(
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
views.api_reschedule_candidate_meeting,
name="api_reschedule_candidate_meeting",
),
# New URL for simple page-based meeting scheduling
path('jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/', views.schedule_meeting_for_candidate, name='schedule_meeting_for_candidate'),
path('jobs/<slug:slug>/candidates/<int:candidate_pk>/delete_meeting_for_candidate/<int:meeting_id>/', views.delete_meeting_for_candidate, name='delete_meeting_for_candidate'),
path(
"jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/",
views.schedule_meeting_for_candidate,
name="schedule_meeting_for_candidate",
),
path(
"jobs/<slug:slug>/candidates/<int:candidate_pk>/delete_meeting_for_candidate/<int:meeting_id>/",
views.delete_meeting_for_candidate,
name="delete_meeting_for_candidate",
),
# users urls
path('user/<int:pk>',views.user_detail,name='user_detail'),
path('user/user_profile_image_update/<int:pk>',views.user_profile_image_update,name='user_profile_image_update'),
path('easy_logs/',views.easy_logs,name='easy_logs'),
path('settings/',views.admin_settings,name='admin_settings'),
path('staff/create',views.create_staff_user,name='create_staff_user'),
path('set_staff_password/<int:pk>/',views.set_staff_password,name='set_staff_password'),
path('account_toggle_status/<int:pk>',views.account_toggle_status,name='account_toggle_status'),
path("user/<int:pk>", views.user_detail, name="user_detail"),
path(
"user/user_profile_image_update/<int:pk>",
views.user_profile_image_update,
name="user_profile_image_update",
),
path("easy_logs/", views.easy_logs, name="easy_logs"),
path("settings/", views.admin_settings, name="admin_settings"),
path("staff/create", views.create_staff_user, name="create_staff_user"),
path(
"set_staff_password/<int:pk>/",
views.set_staff_password,
name="set_staff_password",
),
path(
"account_toggle_status/<int:pk>",
views.account_toggle_status,
name="account_toggle_status",
),
# Source URLs
path('sources/', views_source.SourceListView.as_view(), name='source_list'),
path('sources/create/', views_source.SourceCreateView.as_view(), name='source_create'),
path('sources/<int:pk>/', views_source.SourceDetailView.as_view(), name='source_detail'),
path('sources/<int:pk>/update/', views_source.SourceUpdateView.as_view(), name='source_update'),
path('sources/<int:pk>/delete/', views_source.SourceDeleteView.as_view(), name='source_delete'),
path('sources/<int:pk>/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'),
path('sources/<int:pk>/toggle-status/', views_source.toggle_source_status_view, name='toggle_source_status'),
path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'),
path("sources/", views_source.SourceListView.as_view(), name="source_list"),
path(
"sources/create/", views_source.SourceCreateView.as_view(), name="source_create"
),
path(
"sources/<int:pk>/",
views_source.SourceDetailView.as_view(),
name="source_detail",
),
path(
"sources/<int:pk>/update/",
views_source.SourceUpdateView.as_view(),
name="source_update",
),
path(
"sources/<int:pk>/delete/",
views_source.SourceDeleteView.as_view(),
name="source_delete",
),
path(
"sources/<int:pk>/generate-keys/",
views_source.generate_api_keys_view,
name="generate_api_keys",
),
path(
"sources/<int:pk>/toggle-status/",
views_source.toggle_source_status_view,
name="toggle_source_status",
),
path(
"sources/api/copy-to-clipboard/",
views_source.copy_to_clipboard_view,
name="copy_to_clipboard",
),
# Meeting Comments URLs
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'),
path('meetings/<slug:slug>/comments/<int:comment_id>/edit/', views.edit_meeting_comment, name='edit_meeting_comment'),
path('meetings/<slug:slug>/comments/<int:comment_id>/delete/', views.delete_meeting_comment, name='delete_meeting_comment'),
path('meetings/<slug:slug>/set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'),
path(
"meetings/<slug:slug>/comments/add/",
views.add_meeting_comment,
name="add_meeting_comment",
),
path(
"meetings/<slug:slug>/comments/<int:comment_id>/edit/",
views.edit_meeting_comment,
name="edit_meeting_comment",
),
path(
"meetings/<slug:slug>/comments/<int:comment_id>/delete/",
views.delete_meeting_comment,
name="delete_meeting_comment",
),
path(
"meetings/<slug:slug>/set_meeting_candidate/",
views.set_meeting_candidate,
name="set_meeting_candidate",
),
# Hiring Agency URLs
path('agencies/', views.agency_list, name='agency_list'),
path('agencies/create/', views.agency_create, name='agency_create'),
path('agencies/<slug:slug>/', views.agency_detail, name='agency_detail'),
path('agencies/<slug:slug>/update/', views.agency_update, name='agency_update'),
path('agencies/<slug:slug>/delete/', views.agency_delete, name='agency_delete'),
path('agencies/<slug:slug>/candidates/', views.agency_candidates, name='agency_candidates'),
path("agencies/", views.agency_list, name="agency_list"),
path("agencies/create/", views.agency_create, name="agency_create"),
path("agencies/<slug:slug>/", views.agency_detail, name="agency_detail"),
path("agencies/<slug:slug>/update/", views.agency_update, name="agency_update"),
path("agencies/<slug:slug>/delete/", views.agency_delete, name="agency_delete"),
path(
"agencies/<slug:slug>/candidates/",
views.agency_candidates,
name="agency_candidates",
),
# path('agencies/<slug:slug>/send-message/', views.agency_detail_send_message, name='agency_detail_send_message'),
# Agency Assignment Management URLs
path('agency-assignments/', views.agency_assignment_list, name='agency_assignment_list'),
path('agency-assignments/create/', views.agency_assignment_create, name='agency_assignment_create'),
path('agency-assignments/<slug:slug>/create/', views.agency_assignment_create, name='agency_assignment_create'),
path('agency-assignments/<slug:slug>/', views.agency_assignment_detail, name='agency_assignment_detail'),
path('agency-assignments/<slug:slug>/update/', views.agency_assignment_update, name='agency_assignment_update'),
path('agency-assignments/<slug:slug>/extend-deadline/', views.agency_assignment_extend_deadline, name='agency_assignment_extend_deadline'),
path(
"agency-assignments/",
views.agency_assignment_list,
name="agency_assignment_list",
),
path(
"agency-assignments/create/",
views.agency_assignment_create,
name="agency_assignment_create",
),
path(
"agency-assignments/<slug:slug>/create/",
views.agency_assignment_create,
name="agency_assignment_create",
),
path(
"agency-assignments/<slug:slug>/",
views.agency_assignment_detail,
name="agency_assignment_detail",
),
path(
"agency-assignments/<slug:slug>/update/",
views.agency_assignment_update,
name="agency_assignment_update",
),
path(
"agency-assignments/<slug:slug>/extend-deadline/",
views.agency_assignment_extend_deadline,
name="agency_assignment_extend_deadline",
),
# Agency Access Link URLs
path('agency-access-links/create/', views.agency_access_link_create, name='agency_access_link_create'),
path('agency-access-links/<slug:slug>/', views.agency_access_link_detail, name='agency_access_link_detail'),
path('agency-access-links/<slug:slug>/deactivate/', views.agency_access_link_deactivate, name='agency_access_link_deactivate'),
path('agency-access-links/<slug:slug>/reactivate/', views.agency_access_link_reactivate, name='agency_access_link_reactivate'),
path(
"agency-access-links/create/",
views.agency_access_link_create,
name="agency_access_link_create",
),
path(
"agency-access-links/<slug:slug>/",
views.agency_access_link_detail,
name="agency_access_link_detail",
),
path(
"agency-access-links/<slug:slug>/deactivate/",
views.agency_access_link_deactivate,
name="agency_access_link_deactivate",
),
path(
"agency-access-links/<slug:slug>/reactivate/",
views.agency_access_link_reactivate,
name="agency_access_link_reactivate",
),
# Admin Message Center URLs (messaging functionality removed)
# path('admin/messages/', views.admin_message_center, name='admin_message_center'),
# path('admin/messages/compose/', views.admin_compose_message, name='admin_compose_message'),
@ -185,35 +473,72 @@ urlpatterns = [
# path('admin/messages/<int:message_id>/reply/', views.admin_message_reply, name='admin_message_reply'),
# path('admin/messages/<int:message_id>/mark-read/', views.admin_mark_message_read, name='admin_mark_message_read'),
# path('admin/messages/<int:message_id>/delete/', views.admin_delete_message, name='admin_delete_message'),
# Agency Portal URLs (for external agencies)
path('portal/login/', views.agency_portal_login, name='agency_portal_login'),
path('portal/dashboard/', views.agency_portal_dashboard, name='agency_portal_dashboard'),
path('portal/assignment/<slug:slug>/', views.agency_portal_assignment_detail, name='agency_portal_assignment_detail'),
path('portal/assignment/<slug:slug>/submit-candidate/', views.agency_portal_submit_candidate_page, name='agency_portal_submit_candidate_page'),
path('portal/submit-candidate/', views.agency_portal_submit_candidate, name='agency_portal_submit_candidate'),
path('portal/logout/', views.agency_portal_logout, name='agency_portal_logout'),
path("portal/login/", views.agency_portal_login, name="agency_portal_login"),
path(
"portal/dashboard/",
views.agency_portal_dashboard,
name="agency_portal_dashboard",
),
# Unified Portal URLs
path("login/", views.portal_login, name="portal_login"),
path(
"candidate/dashboard/",
views.candidate_portal_dashboard,
name="candidate_portal_dashboard",
),
path(
"portal/dashboard/",
views.agency_portal_dashboard,
name="agency_portal_dashboard",
),
path(
"portal/persons/",
views.agency_portal_persons_list,
name="agency_portal_persons_list",
),
path(
"portal/assignment/<slug:slug>/",
views.agency_portal_assignment_detail,
name="agency_portal_assignment_detail",
),
path(
"portal/assignment/<slug:slug>/submit-candidate/",
views.agency_portal_submit_candidate_page,
name="agency_portal_submit_candidate_page",
),
path(
"portal/submit-candidate/",
views.agency_portal_submit_candidate,
name="agency_portal_submit_candidate",
),
path("portal/logout/", views.portal_logout, name="portal_logout"),
# Agency Portal Candidate Management URLs
path('portal/candidates/<int:candidate_id>/edit/', views.agency_portal_edit_candidate, name='agency_portal_edit_candidate'),
path('portal/candidates/<int:candidate_id>/delete/', views.agency_portal_delete_candidate, name='agency_portal_delete_candidate'),
path(
"portal/candidates/<int:candidate_id>/edit/",
views.agency_portal_edit_candidate,
name="agency_portal_edit_candidate",
),
path(
"portal/candidates/<int:candidate_id>/delete/",
views.agency_portal_delete_candidate,
name="agency_portal_delete_candidate",
),
# API URLs for messaging (removed)
# path('api/agency/messages/<int:message_id>/', views.api_agency_message_detail, name='api_agency_message_detail'),
# path('api/agency/messages/<int:message_id>/mark-read/', views.api_agency_mark_message_read, name='api_agency_mark_message_read'),
# API URLs for candidate management
path('api/candidate/<int:candidate_id>/', views.api_candidate_detail, name='api_candidate_detail'),
path(
"api/candidate/<int:candidate_id>/",
views.api_candidate_detail,
name="api_candidate_detail",
),
# # Admin Notification API
# path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'),
# # Agency Notification API
# path('api/agency/notification-count/', views.api_notification_count, name='api_agency_notification_count'),
# # SSE Notification Stream
# path('api/notifications/stream/', views.notification_stream, name='notification_stream'),
# # Notification URLs
# path('notifications/', views.notification_list, name='notification_list'),
# path('notifications/<int:notification_id>/', views.notification_detail, name='notification_detail'),
@ -222,27 +547,63 @@ urlpatterns = [
# path('notifications/<int:notification_id>/delete/', views.notification_delete, name='notification_delete'),
# path('notifications/mark-all-read/', views.notification_mark_all_read, name='notification_mark_all_read'),
# path('api/notification-count/', views.api_notification_count, name='api_notification_count'),
#participants urls
path('participants/', views_frontend.ParticipantsListView.as_view(), name='participants_list'),
path('participants/create/', views_frontend.ParticipantsCreateView.as_view(), name='participants_create'),
path('participants/<slug:slug>/', views_frontend.ParticipantsDetailView.as_view(), name='participants_detail'),
path('participants/<slug:slug>/update/', views_frontend.ParticipantsUpdateView.as_view(), name='participants_update'),
path('participants/<slug:slug>/delete/', views_frontend.ParticipantsDeleteView.as_view(), name='participants_delete'),
# participants urls
path(
"participants/",
views_frontend.ParticipantsListView.as_view(),
name="participants_list",
),
path(
"participants/create/",
views_frontend.ParticipantsCreateView.as_view(),
name="participants_create",
),
path(
"participants/<slug:slug>/",
views_frontend.ParticipantsDetailView.as_view(),
name="participants_detail",
),
path(
"participants/<slug:slug>/update/",
views_frontend.ParticipantsUpdateView.as_view(),
name="participants_update",
),
path(
"participants/<slug:slug>/delete/",
views_frontend.ParticipantsDeleteView.as_view(),
name="participants_delete",
),
# Email composition URLs
path(
"jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/compose-email/",
views.compose_candidate_email,
name="compose_candidate_email",
),
# Message URLs
path("messages/", views.message_list, name="message_list"),
path("messages/create/", views.message_create, name="message_create"),
path("messages/<int:message_id>/", views.message_detail, name="message_detail"),
path("messages/<int:message_id>/reply/", views.message_reply, name="message_reply"),
path("messages/<int:message_id>/mark-read/", views.message_mark_read, name="message_mark_read"),
path("messages/<int:message_id>/mark-unread/", views.message_mark_unread, name="message_mark_unread"),
path("messages/<int:message_id>/delete/", views.message_delete, name="message_delete"),
path("api/unread-count/", views.api_unread_count, name="api_unread_count"),
# Documents
path("documents/upload/<int:application_id>/", views.document_upload, name="document_upload"),
path("documents/<int:document_id>/delete/", views.document_delete, name="document_delete"),
path("documents/<int:document_id>/download/", views.document_download, name="document_download"),
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'),
path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'),
path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'),
# # --- SCHEDULED INTERVIEW URLS (New Centralized Management) ---
# path('interview/list/', views.InterviewListView.as_view(), name='interview_list'),
# path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'),
# path('interviews/<slug:slug>/update/', views.ScheduledInterviewUpdateView.as_view(), name='update_scheduled_interview'),
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
]

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,9 @@ from django.utils import timezone
from datetime import timedelta
import json
# Add imports for user type restrictions
from recruitment.decorators import StaffRequiredMixin, staff_user_required
from datastar_py.django import (
DatastarResponse,
@ -39,7 +42,7 @@ from datastar_py.django import (
# from rich import print
from rich.markdown import CodeBlock
class JobListView(LoginRequiredMixin, ListView):
class JobListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.JobPosting
template_name = 'jobs/job_list.html'
context_object_name = 'jobs'
@ -47,7 +50,6 @@ class JobListView(LoginRequiredMixin, ListView):
def get_queryset(self):
queryset = super().get_queryset().order_by('-created_at')
# Handle search
search_query = self.request.GET.get('search', '')
if search_query:
@ -58,24 +60,23 @@ class JobListView(LoginRequiredMixin, ListView):
)
# Filter for non-staff users
if not self.request.user.is_staff:
queryset = queryset.filter(status='Published')
# if not self.request.user.is_staff:
# queryset = queryset.filter(status='Published')
status=self.request.GET.get('status')
status = self.request.GET.get('status')
if status:
queryset=queryset.filter(status=status)
queryset = queryset.filter(status=status)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('search', '')
context['lang'] = get_language()
return context
class JobCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
class JobCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
model = models.JobPosting
form_class = forms.JobPostingForm
template_name = 'jobs/create_job.html'
@ -83,7 +84,7 @@ class JobCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
success_message = 'Job created successfully.'
class JobUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class JobUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.JobPosting
form_class = forms.JobPostingForm
template_name = 'jobs/edit_job.html'
@ -92,27 +93,25 @@ class JobUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
slug_url_kwarg = 'slug'
class JobDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
class JobDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.JobPosting
template_name = 'jobs/partials/delete_modal.html'
success_url = reverse_lazy('job_list')
success_message = 'Job deleted successfully.'
slug_url_kwarg = 'slug'
class JobCandidatesListView(LoginRequiredMixin, ListView):
model = models.Candidate
class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.Application
template_name = 'jobs/job_candidates_list.html'
context_object_name = 'candidates'
context_object_name = 'applications'
paginate_by = 10
def get_queryset(self):
# Get the job by slug
self.job = get_object_or_404(models.JobPosting, slug=self.kwargs['slug'])
# Filter candidates for this specific job
queryset = models.Candidate.objects.filter(job=self.job)
queryset = models.Application.objects.filter(job=self.job)
if self.request.GET.get('stage'):
stage=self.request.GET.get('stage')
@ -132,7 +131,7 @@ class JobCandidatesListView(LoginRequiredMixin, ListView):
# Filter for non-staff users
if not self.request.user.is_staff:
return models.Candidate.objects.none() # Restrict for non-staff
return models.Application.objects.none() # Restrict for non-staff
return queryset.order_by('-created_at')
@ -143,10 +142,10 @@ class JobCandidatesListView(LoginRequiredMixin, ListView):
return context
class CandidateListView(LoginRequiredMixin, ListView):
model = models.Candidate
class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.Application
template_name = 'recruitment/candidate_list.html'
context_object_name = 'candidates'
context_object_name = 'applications'
paginate_by = 100
def get_queryset(self):
@ -156,22 +155,22 @@ class CandidateListView(LoginRequiredMixin, ListView):
search_query = self.request.GET.get('search', '')
job = self.request.GET.get('job', '')
stage = self.request.GET.get('stage', '')
if search_query:
queryset = queryset.filter(
Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) |
Q(email__icontains=search_query) |
Q(phone__icontains=search_query) |
Q(stage__icontains=search_query) |
Q(job__title__icontains=search_query)
)
# if search_query:
# queryset = queryset.filter(
# Q(first_name__icontains=search_query) |
# Q(last_name__icontains=search_query) |
# Q(email__icontains=search_query) |
# Q(phone__icontains=search_query) |
# Q(stage__icontains=search_query) |
# Q(job__title__icontains=search_query)
# )
if job:
queryset = queryset.filter(job__slug=job)
if stage:
queryset = queryset.filter(stage=stage)
# Filter for non-staff users
if not self.request.user.is_staff:
return models.Candidate.objects.none() # Restrict for non-staff
# if not self.request.user.is_staff:
# return models.Application.objects.none() # Restrict for non-staff
return queryset.order_by('-created_at')
@ -184,9 +183,9 @@ class CandidateListView(LoginRequiredMixin, ListView):
return context
class CandidateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = models.Candidate
form_class = forms.CandidateForm
class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
model = models.Application
form_class = forms.ApplicationForm
template_name = 'recruitment/candidate_create.html'
success_url = reverse_lazy('candidate_list')
success_message = 'Candidate created successfully.'
@ -204,18 +203,23 @@ class CandidateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
form.instance.job = job
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.method == 'GET':
context['person_form'] = forms.PersonForm()
return context
class CandidateUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Candidate
form_class = forms.CandidateForm
class ApplicationUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Application
form_class = forms.ApplicationForm
template_name = 'recruitment/candidate_update.html'
success_url = reverse_lazy('candidate_list')
success_message = 'Candidate updated successfully.'
slug_url_kwarg = 'slug'
class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Candidate
class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Application
template_name = 'recruitment/candidate_delete.html'
success_url = reverse_lazy('candidate_list')
success_message = 'Candidate deleted successfully.'
@ -225,28 +229,30 @@ class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
def retry_scoring_view(request,slug):
from django_q.tasks import async_task
candidate = get_object_or_404(models.Candidate, slug=slug)
application = get_object_or_404(models.Application, slug=slug)
async_task(
'recruitment.tasks.handle_reume_parsing_and_scoring',
candidate.pk,
application.pk,
hook='recruitment.hooks.callback_ai_parsing',
sync=True,
)
return redirect('candidate_detail', slug=candidate.slug)
)
return redirect('candidate_detail', slug=application.slug)
@login_required
@staff_user_required
def training_list(request):
materials = models.TrainingMaterial.objects.all().order_by('-created_at')
return render(request, 'recruitment/training_list.html', {'materials': materials})
@login_required
@staff_user_required
def candidate_detail(request, slug):
from rich.json import JSON
candidate = get_object_or_404(models.Candidate, slug=slug)
candidate = get_object_or_404(models.Application, slug=slug)
try:
parsed = ast.literal_eval(candidate.parsed_summary)
except:
@ -255,9 +261,10 @@ def candidate_detail(request, slug):
# Create stage update form for staff users
stage_form = None
if request.user.is_staff:
stage_form = forms.CandidateStageForm()
stage_form = forms.ApplicationStageForm()
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
# parsed = json_to_markdown_table([parsed])
return render(request, 'recruitment/candidate_detail.html', {
@ -268,31 +275,33 @@ def candidate_detail(request, slug):
@login_required
@staff_user_required
def candidate_resume_template_view(request, slug):
"""Display formatted resume template for a candidate"""
candidate = get_object_or_404(models.Candidate, slug=slug)
application = get_object_or_404(models.Application, slug=slug)
if not request.user.is_staff:
messages.error(request, _("You don't have permission to view this page."))
return redirect('candidate_list')
return render(request, 'recruitment/candidate_resume_template.html', {
'candidate': candidate
'application': application
})
@login_required
@staff_user_required
def candidate_update_stage(request, slug):
"""Handle HTMX stage update requests"""
candidate = get_object_or_404(models.Candidate, slug=slug)
form = forms.CandidateStageForm(request.POST, instance=candidate)
application = get_object_or_404(models.Application, slug=slug)
form = forms.ApplicationStageForm(request.POST, instance=application)
if form.is_valid():
stage_value = form.cleaned_data['stage']
candidate.stage = stage_value
candidate.save(update_fields=['stage'])
messages.success(request,"Candidate Stage Updated")
return redirect("candidate_detail",slug=candidate.slug)
application.stage = stage_value
application.save(update_fields=['stage'])
messages.success(request,"application Stage Updated")
return redirect("candidate_detail",slug=application.slug)
class TrainingListView(LoginRequiredMixin, ListView):
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.TrainingMaterial
template_name = 'recruitment/training_list.html'
context_object_name = 'materials'
@ -320,7 +329,7 @@ class TrainingListView(LoginRequiredMixin, ListView):
return context
class TrainingCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
class TrainingCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
model = models.TrainingMaterial
form_class = forms.TrainingMaterialForm
template_name = 'recruitment/training_create.html'
@ -332,7 +341,7 @@ class TrainingCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
return super().form_valid(form)
class TrainingUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class TrainingUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.TrainingMaterial
form_class = forms.TrainingMaterialForm
template_name = 'recruitment/training_update.html'
@ -341,13 +350,13 @@ class TrainingUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
slug_url_kwarg = 'slug'
class TrainingDetailView(LoginRequiredMixin, DetailView):
class TrainingDetailView(LoginRequiredMixin, StaffRequiredMixin, DetailView):
model = models.TrainingMaterial
template_name = 'recruitment/training_detail.html'
context_object_name = 'material'
slug_url_kwarg = 'slug'
class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
class TrainingDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.TrainingMaterial
template_name = 'recruitment/training_delete.html'
success_url = reverse_lazy('training_list')
@ -355,7 +364,7 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
# IMPORTANT: Ensure 'models' correctly refers to your Django models file
# Example: from . import models
# Example: from . import models
# --- Constants ---
SCORE_PATH = 'ai_analysis_data__analysis_data__match_score'
@ -365,27 +374,28 @@ TARGET_TIME_TO_HIRE_DAYS = 45 # Used for the template visualization
@login_required
@staff_user_required
def dashboard_view(request):
selected_job_pk = request.GET.get('selected_job_pk')
today = timezone.now().date()
# --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) ---
all_jobs_queryset = models.JobPosting.objects.all().order_by('-created_at')
all_candidates_queryset = models.Candidate.objects.all()
all_candidates_queryset = models.Application.objects.all()
# Global KPI Card Metrics
total_jobs_global = all_jobs_queryset.count()
total_participants = models.Participants.objects.count()
total_jobs_posted_linkedin = all_jobs_queryset.filter(linkedin_post_id__isnull=False).count()
# Data for Job App Count Chart (always for ALL jobs)
job_titles = [job.title for job in all_jobs_queryset]
job_app_counts = [job.candidates.count() for job in all_jobs_queryset]
job_app_counts = [job.applications.count() for job in all_jobs_queryset]
# --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS ---
# Group ALL candidates by creation date
global_daily_applications_qs = all_candidates_queryset.annotate(
date=TruncDate('created_at')
@ -398,22 +408,22 @@ def dashboard_view(request):
# --- 3. FILTERING LOGIC: Determine the scope for scoped metrics ---
candidate_queryset = all_candidates_queryset
job_scope_queryset = all_jobs_queryset
interview_queryset = models.ScheduledInterview.objects.all()
current_job = None
if selected_job_pk:
# Filter all base querysets
candidate_queryset = candidate_queryset.filter(job__pk=selected_job_pk)
interview_queryset = interview_queryset.filter(job__pk=selected_job_pk)
try:
current_job = all_jobs_queryset.get(pk=selected_job_pk)
job_scope_queryset = models.JobPosting.objects.filter(pk=selected_job_pk)
except models.JobPosting.DoesNotExist:
pass
pass
# --- 4. TIME SERIES: SCOPED DAILY APPLICANTS ---
@ -426,15 +436,15 @@ def dashboard_view(request):
).values('date').annotate(
count=Count('pk')
).order_by('date')
scoped_dates = [item['date'].strftime('%Y-%m-%d') for item in scoped_daily_applications_qs]
scoped_counts = [item['count'] for item in scoped_daily_applications_qs]
# --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) ---
total_candidates = candidate_queryset.count()
candidates_with_score_query = candidate_queryset.filter(
is_resume_parsed=True
).annotate(
@ -448,11 +458,11 @@ def dashboard_view(request):
total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count()
last_week = timezone.now() - timedelta(days=7)
new_candidates_7days = candidate_queryset.filter(created_at__gte=last_week).count()
open_positions_agg = job_scope_queryset.filter(status="ACTIVE").aggregate(total_open=Sum('open_positions'))
total_open_positions = open_positions_agg['total_open'] or 0
average_applications_result = job_scope_queryset.annotate(
candidate_count=Count('candidates', distinct=True)
candidate_count=Count('applications', distinct=True)
).aggregate(avg_apps=Avg('candidate_count'))['avg_apps']
average_applications = round(average_applications_result or 0, 2)
@ -463,21 +473,24 @@ def dashboard_view(request):
)
lst=[c.time_to_hire_days for c in hired_candidates]
time_to_hire_query = hired_candidates.annotate(
time_diff=ExpressionWrapper(
F('hired_date') - F('created_at__date'),
F('join_date') - F('created_at__date'),
output_field=fields.DurationField()
)
).aggregate(avg_time_to_hire=Avg('time_diff'))
print(time_to_hire_query)
avg_time_to_hire_days = (
time_to_hire_query.get('avg_time_to_hire').days
time_to_hire_query.get('avg_time_to_hire').days
if time_to_hire_query.get('avg_time_to_hire') else 0
)
print(avg_time_to_hire_days)
applied_count = candidate_queryset.filter(stage='Applied').count()
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
screening_pass_rate = round( (advanced_count / applied_count) * 100, 1 ) if applied_count > 0 else 0
@ -493,8 +506,8 @@ def dashboard_view(request):
meetings_scheduled_this_week = interview_queryset.filter(
interview_date__week=current_week, interview_date__year=current_year
).count()
avg_match_score_result = candidates_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
avg_match_score = round(avg_match_score_result or 0, 1)
avg_match_score_result = candidates_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
avg_match_score = round(avg_match_score_result or 0, 1)
high_potential_count = candidates_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count()
high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0
total_scored_candidates = candidates_with_score_query.count()
@ -506,15 +519,15 @@ def dashboard_view(request):
# A. Pipeline Funnel (Scoped)
stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage'))
stage_map = {item['stage']: item['count'] for item in stage_counts}
candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
candidates_count = [
stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0),
stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0),
stage_map.get('Offer', 0), stage_map.get('Hired',0)
]
# --- 7. GAUGE CHART CALCULATION (Time-to-Hire) ---
current_days = avg_time_to_hire_days
rotation_percent = current_days / MAX_TIME_TO_HIRE_DAYS if MAX_TIME_TO_HIRE_DAYS > 0 else 0
rotation_degrees = rotation_percent * 180
@ -524,20 +537,20 @@ def dashboard_view(request):
hiring_source_counts = candidate_queryset.values('hiring_source').annotate(count=Count('stage'))
source_map= {item['hiring_source']: item['count'] for item in hiring_source_counts}
candidates_count_in_each_source = [
source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0),
source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0),
]
all_hiring_sources=["Public", "Internal", "Agency"]
all_hiring_sources=["Public", "Internal", "Agency"]
# --- 8. CONTEXT RETURN ---
context = {
# Global KPIs
'total_jobs_global': total_jobs_global,
'total_participants': total_participants,
'total_jobs_posted_linkedin': total_jobs_posted_linkedin,
# Scoped KPIs
'total_active_jobs': total_active_jobs,
'total_candidates': total_candidates,
@ -549,16 +562,16 @@ def dashboard_view(request):
'offers_accepted_rate': offers_accepted_rate,
'vacancy_fill_rate': vacancy_fill_rate,
'meetings_scheduled_this_week': meetings_scheduled_this_week,
'avg_match_score': avg_match_score,
'avg_match_score': avg_match_score,
'high_potential_count': high_potential_count,
'high_potential_ratio': high_potential_ratio,
'scored_ratio': scored_ratio,
# Chart Data
'candidate_stage': json.dumps(candidate_stage),
'candidates_count': json.dumps(candidates_count),
'job_titles': json.dumps(job_titles),
'job_app_counts': json.dumps(job_app_counts),
'job_titles': json.dumps(job_titles),
'job_app_counts': json.dumps(job_app_counts),
# 'source_volume_chart_data' is intentionally REMOVED
# Time Series Data
@ -572,7 +585,7 @@ def dashboard_view(request):
'gauge_max_days': MAX_TIME_TO_HIRE_DAYS,
'gauge_target_days': TARGET_TIME_TO_HIRE_DAYS,
'gauge_rotation_degrees': rotation_degrees_final,
# UI Control
'jobs': all_jobs_queryset,
'current_job_id': selected_job_pk,
@ -582,11 +595,12 @@ def dashboard_view(request):
'candidates_count_in_each_source': json.dumps(candidates_count_in_each_source),
'all_hiring_sources': json.dumps(all_hiring_sources),
}
return render(request, 'recruitment/dashboard.html', context)
@login_required
@staff_user_required
def candidate_offer_view(request, slug):
"""View for candidates in the Offer stage"""
job = get_object_or_404(models.JobPosting, slug=slug)
@ -616,6 +630,7 @@ def candidate_offer_view(request, slug):
@login_required
@staff_user_required
def candidate_hired_view(request, slug):
"""View for hired candidates"""
job = get_object_or_404(models.JobPosting, slug=slug)
@ -645,13 +660,16 @@ def candidate_hired_view(request, slug):
@login_required
@staff_user_required
def update_candidate_status(request, job_slug, candidate_slug, stage_type, status):
"""Handle exam/interview/offer status updates"""
from django.utils import timezone
job = get_object_or_404(models.JobPosting, slug=job_slug)
candidate = get_object_or_404(models.Candidate, slug=candidate_slug, job=job)
candidate = get_object_or_404(models.Application, slug=candidate_slug, job=job)
print(stage_type)
print(status)
print(request.method)
if request.method == "POST":
if stage_type == 'exam':
candidate.exam_status = status
@ -709,6 +727,7 @@ STAGE_CONFIG = {
@login_required
@staff_user_required
def export_candidates_csv(request, job_slug, stage):
"""Export candidates for a specific stage as CSV"""
job = get_object_or_404(models.JobPosting, slug=job_slug)
@ -722,9 +741,9 @@ def export_candidates_csv(request, job_slug, stage):
# Filter candidates based on stage
if stage == 'hired':
candidates = job.candidates.filter(**config['filter'])
candidates = job.applications.filter(**config['filter'])
else:
candidates = job.candidates.filter(**config['filter'])
candidates = job.applications.filter(**config['filter'])
# Handle search if provided
search_query = request.GET.get('search', '')
@ -848,6 +867,7 @@ def export_candidates_csv(request, job_slug, stage):
@login_required
@staff_user_required
def sync_hired_candidates(request, job_slug):
"""Sync hired candidates to external sources using Django-Q"""
from django_q.tasks import async_task
@ -886,6 +906,7 @@ def sync_hired_candidates(request, job_slug):
@login_required
@staff_user_required
def test_source_connection(request, source_id):
"""Test connection to an external source"""
from .candidate_sync_service import CandidateSyncService
@ -920,6 +941,7 @@ def test_source_connection(request, source_id):
@login_required
@staff_user_required
def sync_task_status(request, task_id):
"""Check the status of a sync task"""
from django_q.models import Task
@ -971,6 +993,7 @@ def sync_task_status(request, task_id):
@login_required
@staff_user_required
def sync_history(request, job_slug=None):
"""View sync history and logs"""
from .models import IntegrationLog
@ -1005,7 +1028,7 @@ def sync_history(request, job_slug=None):
#participants views
class ParticipantsListView(LoginRequiredMixin, ListView):
class ParticipantsListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.Participants
template_name = 'participants/participants_list.html'
context_object_name = 'participants'
@ -1034,13 +1057,13 @@ class ParticipantsListView(LoginRequiredMixin, ListView):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('search', '')
return context
class ParticipantsDetailView(LoginRequiredMixin, DetailView):
class ParticipantsDetailView(LoginRequiredMixin, StaffRequiredMixin, DetailView):
model = models.Participants
template_name = 'participants/participants_detail.html'
context_object_name = 'participant'
slug_url_kwarg = 'slug'
class ParticipantsCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
model = models.Participants
form_class = forms.ParticipantsForm
template_name = 'participants/participants_create.html'
@ -1054,9 +1077,9 @@ class ParticipantsCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateVie
# initial['jobs'] = [job]
# return initial
class ParticipantsUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class ParticipantsUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Participants
form_class = forms.ParticipantsForm
template_name = 'participants/participants_create.html'
@ -1064,9 +1087,9 @@ class ParticipantsUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateVie
success_message = 'Participant updated successfully.'
slug_url_kwarg = 'slug'
class ParticipantsDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
class ParticipantsDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Participants
success_url = reverse_lazy('participants_list') # Redirect to the participants list after success
success_message = 'Participant deleted successfully.'
slug_url_kwarg = 'slug'
slug_url_kwarg = 'slug'

View File

@ -122,6 +122,11 @@
</ul>
</li>
{% endif %} {% endcomment %}
<li class="nav-item me-2">
<a class="nav-link" href="{% url 'message_list' %}">
<i class="fas fa-envelope"></i>
</a>
</li>
<li class="nav-item dropdown">
<button
@ -237,7 +242,15 @@
<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" %}
{% trans "Applicants" %}
{% trans "Applications" %}
</span>
</a>
</li>
<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" %}
{% trans "Person" %}
</span>
</a>
</li>
@ -340,6 +353,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Navbar collapse auto-close on link click (Standard Mobile UX)
@ -404,6 +418,23 @@
</script>
<!-- Message Count JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update unread message count on page load
fetch('/api/unread-count/')
.then(response => response.json())
.then(data => {
const badge = document.getElementById('unread-messages-badge');
if (badge && data.unread_count > 0) {
badge.textContent = data.unread_count;
badge.style.display = 'inline-block';
}
})
.catch(error => console.error('Error fetching unread count:', error));
});
</script>
<!-- Notification JavaScript for Admin Users -->
{% comment %} {% if request.user.is_authenticated and request.user.is_staff %}
<script>

View File

@ -0,0 +1,149 @@
{% load static %}
{% load file_filters %}
<div class="card shadow-sm">
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0 text-primary">Documents</h5>
<button
type="button"
class="btn bg-primary-theme text-white btn-sm"
data-bs-toggle="modal"
data-bs-target="#documentUploadModal"
>
<i class="fas fa-plus me-2"></i>Upload Document
</button>
</div>
<!-- Document Upload Modal -->
<div class="modal fade" id="documentUploadModal" tabindex="-1" aria-labelledby="documentUploadModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="documentUploadModalLabel">Upload Document</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form
method="post"
enctype="multipart/form-data"
hx-post="{% url 'document_upload' candidate.id %}"
hx-target="#documents-pane"
hx-select="#documents-pane"
hx-swap="outerHTML"
hx-on::after-request="bootstrap.Modal.getInstance(document.getElementById('documentUploadModal')).hide()"
>
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label for="documentType" class="form-label">Document Type</label>
<select name="document_type" id="documentType" class="form-select">
<option value="resume">Resume</option>
<option value="cover_letter">Cover Letter</option>
<option value="portfolio">Portfolio</option>
<option value="certificate">Certificate</option>
<option value="id_proof">ID Proof</option>
<option value="other">Other</option>
</select>
</div>
<div class="mb-3">
<label for="documentFile" class="form-label">File</label>
<input
type="file"
name="file"
id="documentFile"
class="form-control"
required
>
</div>
<div class="mb-3">
<label for="documentDescription" class="form-label">Description</label>
<textarea
name="description"
id="documentDescription"
rows="3"
class="form-control"
placeholder="Optional description..."
></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>Upload
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Documents List -->
<div class="card-body" id="document-list-container">
{% if documents %}
{% for document in documents %}
<div class="d-flex justify-content-between align-items-center p-3 border-bottom hover-bg-light">
<div class="d-flex align-items-center">
<i class="fas fa-file text-primary me-3"></i>
<div>
<div class="fw-medium text-dark">{{ document.get_document_type_display }}</div>
<div class="small text-muted">{{ document.file.name|filename }}</div>
{% if document.description %}
<div class="small text-muted">{{ document.description }}</div>
{% endif %}
<div class="small text-muted">
Uploaded by {{ document.uploaded_by.get_full_name|default:document.uploaded_by.username }} on {{ document.created_at|date:"M d, Y" }}
</div>
</div>
</div>
<div class="d-flex align-items-center">
<a
href="{% url 'document_download' document.id %}"
class="btn btn-sm btn-outline-primary me-2"
title="Download"
>
<i class="fas fa-download"></i>
</a>
{% if user.is_superuser or candidate.job.assigned_to == user %}
<button
type="button"
class="btn btn-sm btn-outline-danger"
onclick="confirmDelete({{ document.id }}, '{{ document.file.name|filename|default:"Document" }}')"
title="Delete"
>
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-5 text-muted">
<i class="fas fa-file-alt fa-3x mb-3"></i>
<p class="mb-2">No documents uploaded yet.</p>
<p class="small">Click "Upload Document" to add files for this candidate.</p>
</div>
{% endif %}
</div>
</div>
<style>
.hover-bg-light:hover {
background-color: #f8f9fa;
transition: background-color 0.2s ease;
}
</style>
<script>
function confirmDelete(documentId, fileName) {
if (confirm(`Are you sure you want to delete "${fileName}"?`)) {
htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), {
target: '#document-list-container',
swap: 'innerHTML'
});
}
}
</script>

View File

@ -4,24 +4,24 @@
{# Helper to build the query string while excluding the 'page' parameter #}
{% load url_extras %}
{# Build a string of all current filters (e.g., &department=IT&type=FULL_TIME) #}
{% add_get_params request.GET as filter_params %}
{% with filter_params=filter_params %}
{% if page_obj.has_previous %}
{# First Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
<a class="page-link text-primary-theme"
href="?page=1{{ filter_params }}">
First
</a>
</li>
{# Previous Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
<a class="page-link text-primary-theme"
href="?page={{ page_obj.previous_page_number }}{{ filter_params }}">
Previous
</a>
@ -36,26 +36,26 @@
</li>
{% if page_obj.has_next %}
{# Next Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
<a class="page-link text-primary-theme"
href="?page={{ page_obj.next_page_number }}{{ filter_params }}">
Next
</a>
</li>
{# Last Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
<a class="page-link text-primary-theme"
href="?page={{ page_obj.paginator.num_pages }}{{ filter_params }}">
Last
</a>
</li>
{% endif %}
{% endwith %}
</ul>
</nav>
{% endif %}

View File

@ -65,7 +65,7 @@
padding: 10px 15px;
background-color: var(--kaauh-teal-light);
}
/* FullCalendar Customization */
#calendar {
font-size: 0.9em;
@ -87,7 +87,7 @@
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-5">
<h1 class="h3 page-header">
<i class="fas fa-calendar-alt me-2 text-primary-theme"></i> Interview Schedule Preview: **{{ job.title }}**
@ -98,13 +98,13 @@
<div class="card-body p-4 p-lg-5">
<h4 class="card-title-border">{% trans "Schedule Parameters" %}</h4>
<div class="row g-4">
<div class="col-md-6">
<p class="mb-2"><strong><i class="fas fa-clock me-2 text-primary-theme"></i> Working Hours:</strong> {{ start_time|time:"g:i A" }} to {{ end_time|time:"g:i A" }}</p>
<p class="mb-2"><strong><i class="fas fa-hourglass-half me-2 text-primary-theme"></i> Interview Duration:</strong> {{ interview_duration }} minutes</p>
<p class="mb-2"><strong><i class="fas fa-shield-alt me-2 text-primary-theme"></i> Buffer Time:</strong> {{ buffer_time }} minutes</p>
</div>
<div class="col-md-6">
<p class="mb-2"><strong><i class="fas fa-calendar-day me-2 text-primary-theme"></i> Interview Period:</strong> {{ start_date|date:"F j, Y" }} &mdash; {{ end_date|date:"F j, Y" }}</p>
<p class="mb-2"><strong><i class="fas fa-list-check me-2 text-primary-theme"></i> Active Days:</strong>
@ -122,7 +122,7 @@
<p class="mb-2"><strong><i class="fas fa-calendar-day me-2 text-primary-theme"></i> Interview Type:</strong> {{interview_type}}</p>
</div>
</div>
<h5 class="mt-4 pt-3 border-top">{% trans "Daily Break Times" %}</h5>
{% if breaks %}
<div class="d-flex flex-wrap gap-3 mt-3">
@ -162,9 +162,9 @@
{% for item in schedule %}
<tr>
<td>{{ item.date|date:"F j, Y" }}</td>
<td class="fw-bold text-primary-theme">{{ item.time|time:"g:i A" }}</td>
<td>{{ item.candidate.name }}</td>
<td>{{ item.candidate.email }}</td>
<td>{{ item.time|time:"g:i A" }}</td>
<td>{{ item.applications.name }}</td>
<td>{{ item.applications.email }}</td>
</tr>
{% endfor %}
</tbody>
@ -204,7 +204,7 @@ document.addEventListener('DOMContentLoaded', function() {
start: '{{ item.date|date:"Y-m-d" }}T{{ item.time|time:"H:i:s" }}',
url: '#',
// Use the theme color for candidate events
color: 'var(--kaauh-teal-dark)',
color: 'var(--kaauh-teal-dark)',
extendedProps: {
email: '{{ item.candidate.email }}',
time: '{{ item.time|time:"g:i A" }}'
@ -214,7 +214,7 @@ document.addEventListener('DOMContentLoaded', function() {
{% for break in breaks %}
{
title: 'Break',
// FullCalendar requires a specific date for breaks, using start_date as a placeholder for daily breaks.
// FullCalendar requires a specific date for breaks, using start_date as a placeholder for daily breaks.
// Note: Breaks displayed on the monthly grid will only show on start_date, but weekly/daily view should reflect it daily if implemented correctly in the backend or using recurring events.
start: '{{ start_date|date:"Y-m-d" }}T{{ break.start_time|time:"H:i:s" }}',
end: '{{ start_date|date:"Y-m-d" }}T{{ break.end_time|time:"H:i:s" }}',

View File

@ -130,9 +130,9 @@
<label for="{{ form.candidates.id_for_label }}">
{% trans "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)" %}
</label>
{{ form.candidates }}
{% if form.candidates.errors %}
<div class="text-danger small mt-1">{{ form.candidates.errors }}</div>
{{ form.applications }}
{% if form.applications.errors %}
<div class="text-danger small mt-1">{{ form.applications.errors }}</div>
{% endif %}
</div>
</div>

View File

@ -285,7 +285,7 @@
<th style="width: calc(50% / 7);">{% trans "Offer" %}</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>

View File

@ -319,7 +319,7 @@
<td><strong class="text-primary"><a href="{% url 'meeting_details' meeting.slug %}" class="text-decoration-none text-secondary">{{ meeting.topic }}<a></strong></td>
<td>
{% if meeting.interview %}
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} <i class="fas fa-link"></i></a>
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.application.slug %}">{{ meeting.interview.candidate.name }} <i class="fas fa-link"></i></a>
{% else %}
<button data-bs-toggle="modal"
data-bs-target="#meetingModal"

View File

@ -249,9 +249,9 @@ body {
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-briefcase me-2"></i> {% trans "Interview Detail" %}</h2>
<div class="detail-row-group flex-grow-1">
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Title" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'job_detail' meeting.get_job.slug %}">{{ meeting.get_job.title|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Name" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'candidate_detail' meeting.get_candidate.slug %}">{{ meeting.get_candidate.name|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Email" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'candidate_detail' meeting.get_candidate.slug %}">{{ meeting.get_candidate.email|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Title" %}:</div><div class="detail-value-simple">{{ meeting.get_job.title|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Name" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.candidate_full_name|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Email" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.email|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Type" %}:</div><div class="detail-value-simple">{{ meeting.get_job.job_type|default:"N/A" }}</div></div>
{% if meeting.get_candidate.belong_to_agency %}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Agency" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.hiring_agency.name|default:"N/A" }}</a></div></div>
@ -471,15 +471,15 @@ 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>
@ -487,7 +487,7 @@ body {
</thead>
<tbody>
<tr>
<td>
{{ form.participants.errors }}
@ -498,7 +498,7 @@ body {
</td>
</tr>
</table>
</div>
@ -525,7 +525,7 @@ body {
<form method="post" action="{% url 'send_interview_email' meeting.interview.slug %}">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label for="{{ email_form.subject.id_for_label }}" class="form-label fw-bold">Subject</label>
{{ email_form.subject | add_class:"form-control" }}
@ -551,18 +551,18 @@ body {
</ul>
<div class="tab-content border border-top-0 p-3 bg-light-subtle">
{# --- Candidate/Agency Pane --- #}
<div class="tab-pane fade show active" id="candidate-pane" role="tabpanel" aria-labelledby="candidate-tab">
<p class="text-muted small">{% trans "This email will be sent to the candidate or their hiring agency." %}</p>
{% if not candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_candidate.id_for_label }}" class="form-label d-none">{% trans "Candidate Message" %}</label>
{{ email_form.message_for_candidate | add_class:"form-control" }}
</div>
{% endif %}
{% if candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_agency.id_for_label }}" class="form-label d-none">{% trans "Agency Message" %}</label>

View File

@ -0,0 +1,179 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ message.subject }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Message Header -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
{{ message.subject }}
{% if message.parent_message %}
<span class="badge bg-secondary ms-2">Reply</span>
{% endif %}
</h5>
<div class="btn-group" role="group">
<a href="{% url 'message_reply' message.id %}" class="btn btn-outline-info">
<i class="fas fa-reply"></i> Reply
</a>
{% if message.recipient == request.user %}
<a href="{% url 'message_mark_unread' message.id %}"
class="btn btn-outline-warning"
hx-post="{% url 'message_mark_unread' message.id %}">
<i class="fas fa-envelope"></i> Mark Unread
</a>
{% endif %}
<a href="{% url 'message_delete' message.id %}"
class="btn btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}"
hx-confirm="Are you sure you want to delete this message?">
<i class="fas fa-trash"></i> Delete
</a>
<a href="{% url 'message_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Messages
</a>
</div>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<strong>From:</strong>
<span class="text-primary">{{ message.sender.get_full_name|default:message.sender.username }}</span>
</div>
<div class="col-md-6">
<strong>To:</strong>
<span class="text-primary">{{ message.recipient.get_full_name|default:message.recipient.username }}</span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Type:</strong>
<span class="badge bg-{{ message.message_type|lower }}">
{{ message.get_message_type_display }}
</span>
</div>
<div class="col-md-6">
<strong>Status:</strong>
{% if message.is_read %}
<span class="badge bg-success">Read</span>
{% if message.read_at %}
<small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small>
{% endif %}
{% else %}
<span class="badge bg-warning">Unread</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Created:</strong>
<span>{{ message.created_at|date:"M d, Y H:i" }}</span>
</div>
{% if message.job %}
<div class="col-md-6">
<strong>Related Job:</strong>
<a href="{% url 'job_detail' message.job.slug %}" class="text-primary">
{{ message.job.title }}
</a>
</div>
{% endif %}
</div>
{% if message.parent_message %}
<div class="alert alert-info">
<strong>In reply to:</strong>
<a href="{% url 'message_detail' message.parent_message.id %}">
{{ message.parent_message.subject }}
</a>
<small class="text-muted d-block">
From {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }}
on {{ message.parent_message.created_at|date:"M d, Y H:i" }}
</small>
</div>
{% endif %}
</div>
</div>
<!-- Message Content -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">Message</h6>
</div>
<div class="card-body">
<div class="message-content">
{{ message.content|linebreaks }}
</div>
</div>
</div>
<!-- Message Thread (if this is a reply and has replies) -->
{% if message.replies.all %}
<div class="card mt-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-comments"></i> Replies ({{ message.replies.count }})
</h6>
</div>
<div class="card-body">
{% for reply in message.replies.all %}
<div class="border-start ps-3 mb-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<strong>{{ reply.sender.get_full_name|default:reply.sender.username }}</strong>
<small class="text-muted ms-2">
{{ reply.created_at|date:"M d, Y H:i" }}
</small>
</div>
<span class="badge bg-{{ reply.message_type|lower }}">
{{ reply.get_message_type_display }}
</span>
</div>
<div class="reply-content">
{{ reply.content|linebreaks }}
</div>
<div class="mt-2">
<a href="{% url 'message_reply' reply.id %}" class="btn btn-sm btn-outline-info">
<i class="fas fa-reply"></i> Reply to this
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.message-content {
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.6;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 0.375rem;
border: 1px solid #dee2e6;
}
.reply-content {
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.5;
font-size: 0.9rem;
}
.border-start {
border-left: 3px solid #0d6efd;
}
.ps-3 {
padding-left: 1rem;
}
</style>
{% endblock %}

View File

@ -0,0 +1,237 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{% if form.instance.pk %}Reply to Message{% else %}Compose Message{% endif %}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
{% if form.instance.pk %}
<i class="fas fa-reply"></i> Reply to Message
{% else %}
<i class="fas fa-envelope"></i> Compose Message
{% endif %}
</h5>
</div>
<div class="card-body">
{% if form.instance.parent_message %}
<div class="alert alert-info mb-4">
<strong>Replying to:</strong> {{ form.instance.parent_message.subject }}
<br>
<small class="text-muted">
From {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }}
on {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
</small>
<div class="mt-2">
<strong>Original message:</strong>
<div class="border-start ps-3 mt-2">
{{ form.instance.parent_message.content|linebreaks }}
</div>
</div>
</div>
{% endif %}
<form method="post" id="messageForm">
{% csrf_token %}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.recipient.id_for_label }}" class="form-label">
Recipient <span class="text-danger">*</span>
</label>
{{ form.recipient }}
{% if form.recipient.errors %}
<div class="text-danger small mt-1">
{{ form.recipient.errors.0 }}
</div>
{% endif %}
<div class="form-text">
Select the user who will receive this message
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.message_type.id_for_label }}" class="form-label">
Message Type <span class="text-danger">*</span>
</label>
{{ form.message_type }}
{% if form.message_type.errors %}
<div class="text-danger small mt-1">
{{ form.message_type.errors.0 }}
</div>
{% endif %}
<div class="form-text">
Select the type of message you're sending
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.subject.id_for_label }}" class="form-label">
Subject <span class="text-danger">*</span>
</label>
{{ form.subject }}
{% if form.subject.errors %}
<div class="text-danger small mt-1">
{{ form.subject.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.job.id_for_label }}" class="form-label">
Related Job
</label>
{{ form.job }}
{% if form.job.errors %}
<div class="text-danger small mt-1">
{{ form.job.errors.0 }}
</div>
{% endif %}
<div class="form-text">
Optional: Select a job if this message is related to a specific position
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.content.id_for_label }}" class="form-label">
Message <span class="text-danger">*</span>
</label>
{{ form.content }}
{% if form.content.errors %}
<div class="text-danger small mt-1">
{{ form.content.errors.0 }}
</div>
{% endif %}
<div class="form-text">
Write your message here. You can use line breaks and basic formatting.
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'message_list' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i>
{% if form.instance.pk %}
Send Reply
{% else %}
Send Message
{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
#id_content {
min-height: 200px;
resize: vertical;
}
.form-select {
{% if form.recipient.field.widget.attrs.disabled %}
background-color: #f8f9fa;
{% endif %}
}
.border-start {
border-left: 3px solid #0d6efd;
}
.ps-3 {
padding-left: 1rem;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-resize textarea based on content
const textarea = document.getElementById('id_content');
if (textarea) {
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
// Set initial height
textarea.style.height = 'auto';
textarea.style.height = (textarea.scrollHeight) + 'px';
}
// Character counter for subject
const subjectField = document.getElementById('id_subject');
const maxLength = 200;
if (subjectField) {
// Add character counter display
const counter = document.createElement('small');
counter.className = 'text-muted';
counter.style.float = 'right';
subjectField.parentNode.appendChild(counter);
function updateCounter() {
const remaining = maxLength - subjectField.value.length;
counter.textContent = `${subjectField.value.length}/${maxLength} characters`;
if (remaining < 20) {
counter.className = 'text-warning';
} else {
counter.className = 'text-muted';
}
}
subjectField.addEventListener('input', updateCounter);
updateCounter();
}
// Form validation before submit
const form = document.getElementById('messageForm');
if (form) {
form.addEventListener('submit', function(e) {
const content = document.getElementById('id_content').value.trim();
const subject = document.getElementById('id_subject').value.trim();
const recipient = document.getElementById('id_recipient').value;
if (!recipient) {
e.preventDefault();
alert('Please select a recipient.');
return false;
}
if (!subject) {
e.preventDefault();
alert('Please enter a subject.');
return false;
}
if (!content) {
e.preventDefault();
alert('Please enter a message.');
return false;
}
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,230 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Messages{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Messages</h4>
<a href="{% url 'message_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Compose Message
</a>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select name="status" id="status" class="form-select">
<option value="">All Status</option>
<option value="read" {% if status_filter == 'read' %}selected{% endif %}>Read</option>
<option value="unread" {% if status_filter == 'unread' %}selected{% endif %}>Unread</option>
</select>
</div>
<div class="col-md-3">
<label for="type" class="form-label">Type</label>
<select name="type" id="type" class="form-select">
<option value="">All Types</option>
<option value="GENERAL" {% if type_filter == 'GENERAL' %}selected{% endif %}>General</option>
<option value="JOB_RELATED" {% if type_filter == 'JOB_RELATED' %}selected{% endif %}>Job Related</option>
<option value="INTERVIEW" {% if type_filter == 'INTERVIEW' %}selected{% endif %}>Interview</option>
<option value="OFFER" {% if type_filter == 'OFFER' %}selected{% endif %}>Offer</option>
</select>
</div>
<div class="col-md-4">
<label for="q" class="form-label">Search</label>
<div class="input-group">
<input type="text" name="q" id="q" class="form-control"
value="{{ search_query }}" placeholder="Search messages...">
<button class="btn btn-outline-secondary" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-secondary w-100">Filter</button>
</div>
</form>
</div>
</div>
<!-- Statistics -->
<div class="row mb-3">
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">Total Messages</h6>
<h3 class="text-primary">{{ total_messages }}</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">Unread Messages</h6>
<h3 class="text-warning">{{ unread_messages }}</h3>
</div>
</div>
</div>
</div>
<!-- Messages List -->
<div class="card">
<div class="card-body">
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Subject</th>
<th>Sender</th>
<th>Recipient</th>
<th>Type</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for message in page_obj %}
<tr class="{% if not message.is_read %}table-warning{% endif %}">
<td>
<a href="{% url 'message_detail' message.id %}"
class="{% if not message.is_read %}fw-bold{% endif %}">
{{ message.subject }}
</a>
{% if message.parent_message %}
<span class="badge bg-secondary ms-2">Reply</span>
{% endif %}
</td>
<td>{{ message.sender.get_full_name|default:message.sender.username }}</td>
<td>{{ message.recipient.get_full_name|default:message.recipient.username }}</td>
<td>
<span>
{{ message.get_message_type_display }}
</span>
</td>
<td>
{% if message.is_read %}
<span class="badge bg-primary-theme">Read</span>
{% else %}
<span class="badge bg-warning">Unread</span>
{% endif %}
</td>
<td>{{ message.created_at|date:"M d, Y H:i" }}</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'message_detail' message.id %}"
class="btn btn-sm btn-outline-primary" title="View">
<i class="fas fa-eye"></i>
</a>
{% if not message.is_read and message.recipient == request.user %}
<a href="{% url 'message_mark_read' message.id %}"
class="btn btn-sm btn-outline-success"
hx-post="{% url 'message_mark_read' message.id %}"
title="Mark as Read">
<i class="fas fa-check"></i>
</a>
{% endif %}
<a href="{% url 'message_reply' message.id %}"
class="btn btn-sm btn-outline-primary" title="Reply">
<i class="fas fa-reply"></i>
</a>
<a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}"
hx-confirm="Are you sure you want to delete this message?"
title="Delete">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p class="mb-0">No messages found.</p>
<p class="small">Try adjusting your filters or compose a new message.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Message pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center text-muted py-5">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p class="mb-0">No messages found.</p>
<p class="small">Try adjusting your filters or compose a new message.</p>
<a href="{% url 'message_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Compose Message
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Auto-refresh unread count every 30 seconds
setInterval(() => {
fetch('/api/unread-count/')
.then(response => response.json())
.then(data => {
// Update unread count in navigation if it exists
const unreadBadge = document.querySelector('.unread-messages-count');
if (unreadBadge) {
unreadBadge.textContent = data.unread_count;
unreadBadge.style.display = data.unread_count > 0 ? 'inline-block' : 'none';
}
})
.catch(error => console.error('Error fetching unread count:', error));
}, 30000);
</script>
{% endblock %}

View File

@ -0,0 +1,444 @@
{% extends "base.html" %}
{% load static i18n crispy_forms_tags %}
{% block title %}Create Person - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-gray-light: #f8f9fa;
}
/* Form Container Styling */
.form-container {
max-width: 800px;
margin: 0 auto;
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1.5rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Form Field Styling */
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
/* Profile Image Upload Styling */
.profile-image-upload {
border: 2px dashed var(--kaauh-border);
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
}
.profile-image-upload:hover {
border-color: var(--kaauh-teal);
background-color: var(--kaauh-gray-light);
}
.profile-image-preview {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 50%;
border: 3px solid var(--kaauh-teal);
margin: 0 auto 1rem;
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Alert Styling */
.alert {
border-radius: 0.5rem;
border: none;
}
/* Loading State */
.btn.loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="form-container">
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'person_list' %}" class="text-decoration-none">
<i class="fas fa-user-friends me-1"></i> {% trans "People" %}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Create Person" %}</li>
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-plus me-2"></i> {% trans "Create New Person" %}
</h1>
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a>
</div>
<!-- Form Card -->
<div class="card shadow-sm">
<div class="card-body p-4">
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
</h5>
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" 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">
<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>
</div>
<input type="file" name="profile_image" id="id_profile_image"
class="d-none" accept="image/*">
</div>
</div>
</div>
<!-- Personal Information Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-user me-2"></i> {% trans "Personal Information" %}
</h5>
</div>
<div class="col-md-4">
{{ form.first_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.middle_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.last_name|as_crispy_field }}
</div>
</div>
<!-- Contact Information Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-envelope me-2"></i> {% trans "Contact Information" %}
</h5>
</div>
<div class="col-md-6">
{{ form.email|as_crispy_field }}
</div>
<div class="col-md-6">
{{ form.phone|as_crispy_field }}
</div>
</div>
<!-- Additional Information Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-info-circle me-2"></i> {% trans "Additional Information" %}
</h5>
</div>
<div class="col-md-4">
{{ form.date_of_birth|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.nationality|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.gender|as_crispy_field }}
</div>
</div>
<!-- Address Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-map-marker-alt me-2"></i> {% trans "Address" %}
</h5>
</div>
<div class="col-12">
{{ form.address }}
</div>
</div>
<!-- LinkedIn Profile Section -->
{% comment %} <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">
<small class="form-text text-muted">
{% trans "Optional: Add LinkedIn profile URL" %}
</small>
</div>
</div>
</div> {% endcomment %}
<!-- Form Actions -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</a>
<div class="d-flex gap-2">
<button type="reset" class="btn btn-outline-secondary">
<i class="fas fa-undo me-1"></i> {% trans "Reset" %}
</button>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Create Person" %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Profile Image Preview
const profileImageInput = document.getElementById('id_profile_image');
const imagePreviewContainer = document.getElementById('image-preview-container');
profileImageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
imagePreviewContainer.innerHTML = `
<img src="${e.target.result}" alt="Profile Preview" class="profile-image-preview">
<h5 class="text-muted mt-3">${file.name}</h5>
<p class="text-muted small">{% trans "Click to change photo" %}</p>
`;
};
reader.readAsDataURL(file);
}
});
// Form Validation
const form = document.getElementById('person-form');
form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Basic validation
const firstName = document.getElementById('id_first_name').value.trim();
const lastName = document.getElementById('id_last_name').value.trim();
const email = document.getElementById('id_email').value.trim();
if (!firstName || !lastName) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "First name and last name are required." %}');
return;
}
if (email && !isValidEmail(email)) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Please enter a valid email address." %}');
return;
}
});
// Email validation helper
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// LinkedIn URL validation
const linkedinInput = document.getElementById('id_linkedin_profile');
linkedinInput.addEventListener('blur', function() {
const value = this.value.trim();
if (value && !isValidLinkedInURL(value)) {
this.classList.add('is-invalid');
if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = '{% trans "Please enter a valid LinkedIn URL" %}';
this.parentNode.appendChild(feedback);
}
} else {
this.classList.remove('is-invalid');
const feedback = this.parentNode.querySelector('.invalid-feedback');
if (feedback) feedback.remove();
}
});
function isValidLinkedInURL(url) {
const linkedinRegex = /^https?:\/\/(www\.)?linkedin\.com\/.+/i;
return linkedinRegex.test(url);
}
// Drag and Drop functionality
const uploadArea = document.querySelector('.profile-image-upload');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
uploadArea.style.borderColor = 'var(--kaauh-teal)';
uploadArea.style.backgroundColor = 'var(--kaauh-gray-light)';
}
function unhighlight(e) {
uploadArea.style.borderColor = 'var(--kaauh-border)';
uploadArea.style.backgroundColor = 'transparent';
}
uploadArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
profileImageInput.files = files;
const event = new Event('change', { bubbles: true });
profileImageInput.dispatchEvent(event);
}
}
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" />
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('.select2').select2();
});
</script>
{% endblock %}

View File

@ -0,0 +1,607 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{{ person.get_full_name }} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-gray-light: #f8f9fa;
}
/* Profile Header Styling */
.profile-header {
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
color: white;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.profile-image-large {
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 50%;
border: 4px solid white;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: transform 0.2s, box-shadow 0.2s;
background-color: white;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Info Section Styling */
.info-section {
margin-bottom: 1.5rem;
}
.info-section h5 {
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
padding: 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s;
}
.info-item:hover {
background-color: var(--kaauh-gray-light);
}
.info-item i {
color: var(--kaauh-teal);
width: 20px;
margin-right: 1rem;
font-size: 1.1rem;
}
.info-label {
font-weight: 600;
color: var(--kaauh-teal-dark);
min-width: 120px;
}
.info-value {
color: #495057;
flex-grow: 1;
}
/* Badge Styling */
.badge {
font-weight: 600;
padding: 0.4em 0.7em;
border-radius: 0.3rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Related Items Styling */
.related-item {
border-left: 3px solid var(--kaauh-teal);
padding-left: 1rem;
margin-bottom: 1rem;
transition: all 0.2s;
}
.related-item:hover {
border-left-color: var(--kaauh-teal-dark);
background-color: var(--kaauh-gray-light);
border-radius: 0.25rem;
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Empty State Styling */
.empty-state {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
color: var(--kaauh-teal);
opacity: 0.5;
margin-bottom: 1rem;
}
/* Status Indicator */
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 0.5rem;
}
.status-active {
background-color: #28a745;
}
.status-inactive {
background-color: #dc3545;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.profile-header {
text-align: center;
}
.profile-image-large {
margin: 0 auto 1rem;
}
.info-item {
flex-direction: column;
align-items: flex-start;
}
.info-label {
min-width: auto;
margin-bottom: 0.25rem;
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'person_list' %}" class="text-decoration-none">
<i class="fas fa-user-friends me-1"></i> {% trans "People" %}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ person.get_full_name }}</li>
</ol>
</nav>
<!-- Profile Header -->
<div class="profile-header">
<div class="row align-items-center">
<div class="col-md-3 text-center">
{% if person.profile_image %}
<img src="{{ person.profile_image.url }}" alt="{{ person.get_full_name }}"
class="profile-image-large">
{% else %}
<div class="profile-image-large d-flex align-items-center justify-content-center bg-white">
<i class="fas fa-user text-muted fa-3x"></i>
</div>
{% endif %}
</div>
<div class="col-md-9">
<h1 class="display-5 fw-bold mb-2">{{ person.get_full_name }}</h1>
{% if person.email %}
<p class="lead mb-3">
<i class="fas fa-envelope me-2"></i>{{ person.email }}
</p>
{% endif %}
<div class="d-flex flex-wrap gap-2 mb-3">
{% if person.nationality %}
<span class="badge bg-light text-dark">
<i class="fas fa-globe me-1"></i>{{ person.nationality }}
</span>
{% endif %}
{% if person.gender %}
<span class="badge bg-info">
{% if person.gender == 'M' %}{% trans "Male" %}{% else %}{% trans "Female" %}{% endif %}
</span>
{% endif %}
{% if person.user %}
<span class="badge bg-success">
<i class="fas fa-user-check me-1"></i>{% trans "User Account" %}
</span>
{% endif %}
</div>
{% if user.is_staff %}
<div class="d-flex gap-2">
<a href="{% url 'person_update' person.slug %}" class="btn btn-light">
<i class="fas fa-edit me-1"></i> {% trans "Edit Person" %}
</a>
<button type="button" class="btn btn-outline-light"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'person_delete' person.slug %}"
data-item-name="{{ person.get_full_name }}">
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<!-- Personal Information Column -->
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-body">
<div class="info-section">
<h5><i class="fas fa-user me-2"></i>{% trans "Personal Information" %}</h5>
<div class="info-item">
<i class="fas fa-signature"></i>
<span class="info-label">{% trans "Full Name" %}:</span>
<span class="info-value">{{ person.get_full_name }}</span>
</div>
{% if person.first_name %}
<div class="info-item">
<i class="fas fa-user"></i>
<span class="info-label">{% trans "First Name" %}:</span>
<span class="info-value">{{ person.first_name }}</span>
</div>
{% endif %}
{% if person.middle_name %}
<div class="info-item">
<i class="fas fa-user"></i>
<span class="info-label">{% trans "Middle Name" %}:</span>
<span class="info-value">{{ person.middle_name }}</span>
</div>
{% endif %}
{% if person.last_name %}
<div class="info-item">
<i class="fas fa-user"></i>
<span class="info-label">{% trans "Last Name" %}:</span>
<span class="info-value">{{ person.last_name }}</span>
</div>
{% endif %}
{% if person.date_of_birth %}
<div class="info-item">
<i class="fas fa-birthday-cake"></i>
<span class="info-label">{% trans "Date of Birth" %}:</span>
<span class="info-value">{{ person.date_of_birth }}</span>
</div>
{% endif %}
{% if person.gender %}
<div class="info-item">
<i class="fas fa-venus-mars"></i>
<span class="info-label">{% trans "Gender" %}:</span>
<span class="info-value">
{% if person.gender == 'M' %}{% trans "Male" %}{% else %}{% trans "Female" %}{% endif %}
</span>
</div>
{% endif %}
{% if person.nationality %}
<div class="info-item">
<i class="fas fa-globe"></i>
<span class="info-label">{% trans "Nationality" %}:</span>
<span class="info-value">{{ person.nationality }}</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Contact Information Column -->
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-body">
<div class="info-section">
<h5><i class="fas fa-address-book me-2"></i>{% trans "Contact Information" %}</h5>
{% if person.email %}
<div class="info-item">
<i class="fas fa-envelope"></i>
<span class="info-label">{% trans "Email" %}:</span>
<span class="info-value">
<a href="mailto:{{ person.email }}" class="text-decoration-none">
{{ person.email }}
</a>
</span>
</div>
{% endif %}
{% if person.phone %}
<div class="info-item">
<i class="fas fa-phone"></i>
<span class="info-label">{% trans "Phone" %}:</span>
<span class="info-value">
<a href="tel:{{ person.phone }}" class="text-decoration-none">
{{ person.phone }}
</a>
</span>
</div>
{% endif %}
{% if person.address %}
<div class="info-item">
<i class="fas fa-map-marker-alt"></i>
<span class="info-label">{% trans "Address" %}:</span>
<span class="info-value">{{ person.address|linebreaksbr }}</span>
</div>
{% endif %}
{% if person.linkedin_profile %}
<div class="info-item">
<i class="fab fa-linkedin"></i>
<span class="info-label">{% trans "LinkedIn" %}:</span>
<span class="info-value">
<a href="{{ person.linkedin_profile }}" target="_blank"
class="text-decoration-none">
{% trans "View Profile" %}
<i class="fas fa-external-link-alt ms-1"></i>
</a>
</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Related Information -->
<div class="row">
<!-- Applications -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-briefcase me-2"></i>{% trans "Applications" %}
<span class="badge bg-primary ms-2">{{ person.applications.count }}</span>
</h5>
{% if person.applications %}
{% for application in person.applications.all %}
<div class="related-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">
<a href="{% url 'candidate_detail' application.slug %}"
class="text-decoration-none">
{{ application.job.title }}
</a>
</h6>
<small class="text-muted">
{% trans "Applied" %}: {{ application.created_at|date:"d M Y" }}
</small>
</div>
<span class="badge bg-primary">{{ application.stage }}</span>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-briefcase"></i>
<p>{% trans "No applications found" %}</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Documents -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-file-alt me-2"></i>{% trans "Documents" %}
<span class="badge bg-primary ms-2">{{ person.documents.count }}</span>
</h5>
{% if person.documents %}
{% for document in person.documents %}
<div class="related-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">
<a href="{{ document.file.url }}" target="_blank"
class="text-decoration-none">
{{ document.filename }}
</a>
</h6>
<small class="text-muted">
{{ document.file_size|filesizeformat }} •
{{ document.uploaded_at|date:"d M Y" }}
</small>
</div>
<a href="{{ document.file.url }}" download="{{ document.filename }}"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-download"></i>
</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<p>{% trans "No documents found" %}</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-info-circle me-2"></i>{% trans "System Information" %}
</h5>
<div class="row">
<div class="col-md-6">
<div class="info-item">
<i class="fas fa-calendar-plus"></i>
<span class="info-label">{% trans "Created" %}:</span>
<span class="info-value">{{ person.created_at|date:"d M Y H:i" }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<i class="fas fa-calendar-edit"></i>
<span class="info-label">{% trans "Last Updated" %}:</span>
<span class="info-value">{{ person.updated_at|date:"d M Y H:i" }}</span>
</div>
</div>
{% if person.user %}
<div class="col-md-6">
<div class="info-item">
<i class="fas fa-user-shield"></i>
<span class="info-label">{% trans "User Account" %}:</span>
<span class="info-value">
<a href="{% url 'user_detail' person.user.pk %}"
class="text-decoration-none">
{{ person.user.username }}
</a>
</span>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to People" %}
</a>
{% if user.is_staff %}
<div class="d-flex gap-2">
<a href="{% url 'person_update' person.slug %}" class="btn btn-main-action">
<i class="fas fa-edit me-1"></i> {% trans "Edit Person" %}
</a>
<button type="button" class="btn btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'person_delete' person.slug %}"
data-item-name="{{ person.get_full_name }}">
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add smooth scrolling for internal links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Add copy to clipboard functionality for email and phone
const copyElements = document.querySelectorAll('[data-copy]');
copyElements.forEach(element => {
element.addEventListener('click', function() {
const textToCopy = this.getAttribute('data-copy');
navigator.clipboard.writeText(textToCopy).then(() => {
// Show temporary feedback
const originalText = this.innerHTML;
this.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
this.classList.add('text-success');
setTimeout(() => {
this.innerHTML = originalText;
this.classList.remove('text-success');
}, 2000);
});
});
});
// Add hover effects for cards
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,411 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}People - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme (Consistent with Reference) */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-gray-light: #f8f9fa;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
.text-success { color: var(--kaauh-success) !important; }
.text-danger { color: var(--kaauh-danger) !important; }
.text-info { color: #17a2b8 !important; }
/* Enhanced Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: transform 0.2s, box-shadow 0.2s;
background-color: white;
}
.card:not(.no-hover):hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
.card.no-hover:hover {
transform: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Person Card Specifics */
.person-card .card-title {
color: var(--kaauh-teal-dark);
font-weight: 600;
font-size: 1.15rem;
}
.person-card .card-text i {
color: var(--kaauh-teal);
width: 1.25rem;
}
/* Badge Styling */
.badge {
font-weight: 600;
padding: 0.4em 0.7em;
border-radius: 0.3rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Table Styling */
.table-view .table thead th {
background-color: var(--kaauh-teal-dark);
color: white;
font-weight: 600;
border-color: var(--kaauh-border);
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
padding: 1rem;
}
.table-view .table tbody td {
vertical-align: middle;
padding: 1rem;
border-color: var(--kaauh-border);
}
.table-view .table tbody tr:hover {
background-color: var(--kaauh-gray-light);
}
/* Pagination Link Styling */
.pagination .page-item .page-link {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.pagination .page-item:hover .page-link:not(.active) {
background-color: #e9ecef;
}
/* Profile Image Styling */
.profile-image-small {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 50%;
border: 2px solid var(--kaauh-border);
}
.profile-image-medium {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 50%;
border: 3px solid var(--kaauh-teal);
}
/* Filter & Search Layout */
.filter-buttons {
display: flex;
gap: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-friends me-2"></i> {% trans "People Directory" %}
</h1>
<a href="{% url 'person_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New Person" %}
</a>
</div>
<!-- Search and Filters -->
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<form method="get" action="" class="w-100">
<div class="input-group input-group-lg">
<input type="text" name="q" class="form-control" id="search"
placeholder="{% trans 'Search people...' %}"
value="{{ request.GET.q }}">
<button class="btn btn-main-action" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div>
<div class="col-md-6">
<form method="GET" class="row g-3 align-items-end h-100">
{% if request.GET.q %}<input type="hidden" name="q" value="{{ request.GET.q }}">{% endif %}
<div class="col-md-4">
<label for="nationality_filter" class="form-label small text-muted">{% trans "Filter by Nationality" %}</label>
<select name="nationality" id="nationality_filter" class="form-select form-select-sm">
<option value="">{% trans "All Nationalities" %}</option>
{% for nationality in nationalities %}
<option value="{{ nationality }}" {% if request.GET.nationality == nationality %}selected{% endif %}>{{ nationality }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label for="gender_filter" class="form-label small text-muted">{% trans "Filter by Gender" %}</label>
<select name="gender" id="gender_filter" class="form-select form-select-sm">
<option value="">{% trans "All Genders" %}</option>
<option value="M" {% if request.GET.gender == 'M' %}selected{% endif %}>{% trans "Male" %}</option>
<option value="F" {% if request.GET.gender == 'F' %}selected{% endif %}>{% trans "Female" %}</option>
</select>
</div>
<div class="col-md-4 d-flex justify-content-end align-self-end">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
</button>
{% if request.GET.q or request.GET.nationality or request.GET.gender %}
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% if people_list %}
<div id="person-list">
<!-- View Switcher -->
{% include "includes/_list_view_switcher.html" with list_id="person-list" %}
<!-- Table View (Default) -->
<div class="table-view">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th scope="col">{% trans "Photo" %}</th>
<th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Email" %}</th>
<th scope="col">{% trans "Phone" %}</th>
<th scope="col">{% trans "Nationality" %}</th>
<th scope="col">{% trans "Gender" %}</th>
<th scope="col">{% trans "Agency" %}</th>
<th scope="col">{% trans "Created" %}</th>
<th scope="col" class="text-end">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for person in people_list %}
<tr>
<td>
{% if person.profile_image %}
<img src="{{ person.profile_image.url }}" alt="{{ person.get_full_name }}"
class="profile-image-small">
{% else %}
<div class="profile-image-small d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-user text-muted"></i>
</div>
{% endif %}
</td>
<td class="fw-medium">
<a href="{% url 'person_detail' person.slug %}"
class="text-decoration-none link-secondary">
{{ person.full_name }}
</a>
</td>
<td>{{ person.email|default:"N/A" }}</td>
<td>{{ person.phone|default:"N/A" }}</td>
<td>
{% if person.nationality %}
<span class="badge bg-primary">{{ person.nationality }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
{% if person.gender %}
<span class="badge bg-info">
{% if person.gender == 'M' %}{% trans "Male" %}{% else %}{% trans "Female" %}{% endif %}
</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td><span class="badge bg-secondary">{{ person.agency.name|default:"N/A" }}</span></td>
<td>{{ person.created_at|date:"d-m-Y" }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'person_detail' person.slug %}"
class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
</a>
{% if user.is_staff %}
<a href="{% url 'person_update' person.slug %}"
class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger"
title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'person_delete' person.slug %}"
data-item-name="{{ person.get_full_name }}">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Card View -->
<div class="card-view row">
{% for person in people_list %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card person-card h-100 shadow-sm">
<div class="card-body d-flex flex-column">
<div class="d-flex align-items-start mb-3">
<div class="me-3">
{% if person.profile_image %}
<img src="{{ person.profile_image.url }}" alt="{{ person.get_full_name }}"
class="profile-image-medium">
{% else %}
<div class="profile-image-medium d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-user text-muted fa-2x"></i>
</div>
{% endif %}
</div>
<div class="flex-grow-1">
<h5 class="card-title mb-1">
<a href="{% url 'person_detail' person.slug %}"
class="text-decoration-none text-primary-theme">
{{ person.get_full_name }}
</a>
</h5>
<p class="text-muted small mb-2">{{ person.email|default:"N/A" }}</p>
</div>
</div>
<div class="card-text text-muted small">
{% if person.phone %}
<div class="mb-2">
<i class="fas fa-phone me-2"></i>{{ person.phone }}
</div>
{% endif %}
{% if person.nationality %}
<div class="mb-2">
<i class="fas fa-globe me-2"></i>
<span class="badge bg-primary">{{ person.nationality }}</span>
</div>
{% endif %}
{% if person.gender %}
<div class="mb-2">
<i class="fas fa-venus-mars me-2"></i>
<span class="badge bg-info">
{% if person.gender == 'M' %}{% trans "Male" %}{% else %}{% trans "Female" %}{% endif %}
</span>
</div>
{% endif %}
{% if person.date_of_birth %}
<div class="mb-2">
<i class="fas fa-birthday-cake me-2"></i>{{ person.date_of_birth }}
</div>
{% endif %}
</div>
<div class="mt-auto pt-3 border-top">
<div class="d-flex gap-2">
<a href="{% url 'person_detail' person.slug %}"
class="btn btn-sm btn-main-action">
<i class="fas fa-eye"></i> {% trans "View" %}
</a>
{% if user.is_staff %}
<a href="{% url 'person_update' person.slug %}"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-edit"></i> {% trans "Edit" %}
</a>
<button type="button" class="btn btn-outline-danger btn-sm"
title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'person_delete' person.slug %}"
data-item-name="{{ person.get_full_name }}">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Pagination -->
{% include "includes/paginator.html" %}
{% else %}
<!-- Empty State -->
<div class="text-center py-5 card shadow-sm">
<div class="card-body">
<i class="fas fa-user-friends fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
<h3>{% trans "No people found" %}</h3>
<p class="text-muted">{% trans "Create your first person record." %}</p>
{% if user.is_staff %}
<a href="{% url 'person_create' %}" class="btn btn-main-action mt-3">
<i class="fas fa-plus me-1"></i> {% trans "Add Person" %}
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,572 @@
{% extends "base.html" %}
{% load static i18n crispy_forms_tags %}
{% block title %}Update {{ person.get_full_name }} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-gray-light: #f8f9fa;
}
/* Form Container Styling */
.form-container {
max-width: 800px;
margin: 0 auto;
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1.5rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Form Field Styling */
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
/* Profile Image Upload Styling */
.profile-image-upload {
border: 2px dashed var(--kaauh-border);
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
}
.profile-image-upload:hover {
border-color: var(--kaauh-teal);
background-color: var(--kaauh-gray-light);
}
.profile-image-preview {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 50%;
border: 3px solid var(--kaauh-teal);
margin: 0 auto 1rem;
}
.current-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
border: 2px solid var(--kaauh-teal);
margin-right: 1rem;
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Alert Styling */
.alert {
border-radius: 0.5rem;
border: none;
}
/* Loading State */
.btn.loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Current Profile Section */
.current-profile {
background-color: var(--kaauh-gray-light);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.current-profile h6 {
color: var(--kaauh-teal-dark);
font-weight: 600;
margin-bottom: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="form-container">
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'person_list' %}" class="text-decoration-none">
<i class="fas fa-user-friends me-1"></i> {% trans "People" %}
</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'person_detail' person.slug %}" class="text-decoration-none">
{{ person.get_full_name }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Update" %}</li>
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-edit me-2"></i> {% trans "Update Person" %}
</h1>
<div class="d-flex gap-2">
<a href="{% url 'person_detail' person.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a>
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a>
</div>
</div>
<!-- Current Profile Info -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="current-profile">
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
<div class="d-flex align-items-center">
{% if person.profile_image %}
<img src="{{ person.profile_image.url }}" alt="{{ person.get_full_name }}"
class="current-image">
{% else %}
<div class="current-image d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-user text-muted"></i>
</div>
{% endif %}
<div>
<h5 class="mb-1">{{ person.get_full_name }}</h5>
{% if person.email %}
<p class="text-muted mb-0">{{ person.email }}</p>
{% endif %}
<small class="text-muted">
{% trans "Created" %}: {{ person.created_at|date:"d M Y" }} •
{% trans "Last Updated" %}: {{ person.updated_at|date:"d M Y" }}
</small>
</div>
</div>
</div>
</div>
</div>
<!-- Form Card -->
<div class="card shadow-sm">
<div class="card-body p-4">
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
</h5>
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" 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 Person" %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Profile Image Preview
const profileImageInput = document.getElementById('id_profile_image');
const imagePreviewContainer = document.getElementById('image-preview-container');
const originalImage = imagePreviewContainer.innerHTML;
profileImageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
imagePreviewContainer.innerHTML = `
<img src="${e.target.result}" alt="Profile Preview" class="profile-image-preview">
<h5 class="text-muted mt-3">${file.name}</h5>
<p class="text-muted small">{% trans "New photo selected" %}</p>
`;
};
reader.readAsDataURL(file);
} else if (!file) {
// Reset to original if no file selected
imagePreviewContainer.innerHTML = originalImage;
}
});
// Form Validation
const form = document.getElementById('person-form');
form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Basic validation
const firstName = document.getElementById('id_first_name').value.trim();
const lastName = document.getElementById('id_last_name').value.trim();
const email = document.getElementById('id_email').value.trim();
if (!firstName || !lastName) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "First name and last name are required." %}');
return;
}
if (email && !isValidEmail(email)) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Please enter a valid email address." %}');
return;
}
});
// Email validation helper
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// LinkedIn URL validation
const linkedinInput = document.getElementById('id_linkedin_profile');
linkedinInput.addEventListener('blur', function() {
const value = this.value.trim();
if (value && !isValidLinkedInURL(value)) {
this.classList.add('is-invalid');
if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = '{% trans "Please enter a valid LinkedIn URL" %}';
this.parentNode.appendChild(feedback);
}
} else {
this.classList.remove('is-invalid');
const feedback = this.parentNode.querySelector('.invalid-feedback');
if (feedback) feedback.remove();
}
});
function isValidLinkedInURL(url) {
const linkedinRegex = /^https?:\/\/(www\.)?linkedin\.com\/.+/i;
return linkedinRegex.test(url);
}
// Drag and Drop functionality
const uploadArea = document.querySelector('.profile-image-upload');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
uploadArea.style.borderColor = 'var(--kaauh-teal)';
uploadArea.style.backgroundColor = 'var(--kaauh-gray-light)';
}
function unhighlight(e) {
uploadArea.style.borderColor = 'var(--kaauh-border)';
uploadArea.style.backgroundColor = 'transparent';
}
uploadArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
profileImageInput.files = files;
const event = new Event('change', { bubbles: true });
profileImageInput.dispatchEvent(event);
}
}
// Reset button functionality
const resetBtn = form.querySelector('button[type="reset"]');
resetBtn.addEventListener('click', function(e) {
e.preventDefault();
// Reset form fields
form.reset();
// Reset image preview
imagePreviewContainer.innerHTML = originalImage;
// Clear any validation states
form.querySelectorAll('.is-invalid').forEach(element => {
element.classList.remove('is-invalid');
});
// Remove any invalid feedback messages
form.querySelectorAll('.invalid-feedback').forEach(element => {
element.remove();
});
});
// Warn before leaving if changes are made
let formChanged = false;
const formInputs = form.querySelectorAll('input, select, textarea');
formInputs.forEach(input => {
input.addEventListener('change', function() {
formChanged = true;
});
});
window.addEventListener('beforeunload', function(e) {
if (formChanged) {
e.preventDefault();
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
return e.returnValue;
}
});
form.addEventListener('submit', function() {
formChanged = false;
});
});
</script>
{% endblock %}

View File

@ -49,16 +49,23 @@
</div>
</div>
</div>
{# Using inline style for nav background color - replace with a dedicated CSS class (e.g., .bg-kaauh-nav) if defined in main.css #}
<div style="background-color: #00636e;">
<div style="background-color: #00636e;">
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container-fluid" style="max-width: 1600px;">
{% if request.user.user_type == 'candidate' %}
<a class="navbar-brand text-white" href="{% url 'candidate_portal_dashboard' %}" aria-label="Applicant Dashboard">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 40px; height: 40px;">
<span class="ms-3 d-none d-md-inline fw-semibold">{% trans "Applicant Portal" %}</span>
</a>
{% elif request.user.user_type == 'agency' %}
<a class="navbar-brand text-white" href="{% url 'agency_portal_dashboard' %}" aria-label="Agency Dashboard">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 40px; height: 40px;">
<span class="ms-3 d-none d-md-inline fw-semibold">{% trans "Agency Portal" %}</span>
</a>
{% endif %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#agencyNavbar"
aria-controls="agencyNavbar" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
@ -67,8 +74,26 @@
<div class="collapse navbar-collapse" id="agencyNavbar">
<div class="navbar-nav ms-auto">
{# NAVIGATION LINKS (Add your portal links here if needed) #}
{% if request.user.user_type == 'agency' %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'agency_portal_dashboard' %}">
<i class="fas fa-tachometer-alt me-1"></i> {% trans "Dashboard" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'agency_portal_persons_list' %}">
<i class="fas fa-users me-1"></i> {% trans "Persons" %}
</a>
</li>
{% elif request.user.user_type == 'candidate' %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'candidate_portal_dashboard' %}">
<i class="fas fa-tachometer-alt me-1"></i> {% trans "Dashboard" %}
</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
@ -97,7 +122,7 @@
</li>
<li class="nav-item ms-3">
<form method="post" action="{% url 'agency_portal_logout' %}" class="d-inline">
<form method="post" action="{% url 'portal_logout' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-outline-light btn-sm">
<i class="fas fa-sign-out-alt me-1"></i> {% trans "Logout" %}
@ -134,7 +159,11 @@
{% trans "All rights reserved." %}
</p>
<p class="mb-0 text-white-50">
{% trans "Agency Portal" %}
{% if request.user.user_type == 'candidate' %}
{% trans "Candidate Portal" %}
{% elif request.user.user_type == 'agency' %}
{% trans "Agency Portal" %}
{% endif %}
</p>
</div>
</div>
@ -146,7 +175,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script>
{# JavaScript (Left unchanged as it was mostly correct) #}
<script>
document.addEventListener('DOMContentLoaded', () => {
@ -206,4 +235,4 @@
{% block customJS %}{% endblock %}
</body>
</html>
</html>

View File

@ -1,4 +1,4 @@
{% extends 'agency_base.html' %}
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Access Link Details" %} - ATS{% endblock %}

View File

@ -162,7 +162,7 @@
</div>
{% endif %}
</div>
<div class="kaauh-card shadow-sm mb-4">
@ -210,10 +210,12 @@
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit candidates." %}
</div>
<a href="{% url 'agency_access_link_detail' access_link.slug %}"
class="btn btn-outline-info btn-sm mx-2">
<i class="fas fa-eye me-1"></i> {% trans "View Access Links Details" %}
</a>
{% if access_link %}
<a href="{% url 'agency_access_link_detail' access_link.slug %}"
class="btn btn-outline-info btn-sm mx-2">
<i class="fas fa-eye me-1"></i> {% trans "View Access Links Details" %}
</a>
{% endif %}
</div>
</div>
@ -331,7 +333,7 @@
</div>
</div>
<!-- Actions Card -->
<div class="kaauh-card p-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
@ -488,14 +490,14 @@ function copyToClipboard(elementId) {
function confirmDeactivate() {
if (confirm('{% trans "Are you sure you want to deactivate this access link? Agencies will no longer be able to use it." %}')) {
// Submit form to deactivate
window.location.href = '{% url "agency_access_link_deactivate" access_link.slug %}';
window.location.href = '';
}
}
function confirmReactivate() {
if (confirm('{% trans "Are you sure you want to reactivate this access link?" %}')) {
// Submit form to reactivate
window.location.href = '{% url "agency_access_link_reactivate" access_link.slug %}';
window.location.href = '';
}
}

View File

@ -1,4 +1,4 @@
{% extends 'agency_base.html' %}
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{{ assignment.job.title }} - {{ assignment.agency.name }} - Agency Portal{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'agency_base.html' %}
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Agency Dashboard" %} - ATS{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'agency_base.html' %}
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Agency Portal Login" %} - ATS{% endblock %}
@ -132,7 +132,7 @@
<!-- Login Body -->
<div class="login-body">
<!-- Messages -->
<!-- Login Form -->
<form method="post" novalidate>

View File

@ -0,0 +1,390 @@
{% extends 'portal_base.html' %}
{% load static i18n crispy_forms_tags %}
{% block title %}{% trans "Persons List" %} - ATS{% endblock %}
{% block customCSS %}
<style>
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.person-row:hover {
background-color: #f8f9fa;
cursor: pointer;
}
.stage-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-weight: 500;
}
.search-form {
background-color: white;
border: 1px solid var(--kaauh-border);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
</style>
{% endblock%}
{% block content %}
<div class="container-fluid py-4 persons-list">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="px-2 py-2">
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-users me-2"></i>
{% trans "All Persons" %}
</h1>
<p class="text-muted mb-0">
{% trans "All persons who come through" %} {{ agency.name }}
</p>
</div>
<div>
<!-- Add Person Button -->
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#personModal">
<i class="fas fa-plus me-1"></i> {% trans "Add New Person" %}
</button>
</div>
</div>
<!-- Search and Filter Section -->
<div class="kaauh-card shadow-sm mb-4">
<div class="card-body">
<form method="get" class="search-form">
<div class="row g-3">
<div class="col-md-6">
<label for="search" class="form-label fw-semibold">
<i class="fas fa-search me-1"></i>{% trans "Search" %}
</label>
<input type="text"
class="form-control"
id="search"
name="q"
value="{{ search_query }}"
placeholder="{% trans 'Search by name, email, phone, or job title...' %}">
</div>
<div class="col-md-3">
<label for="stage" class="form-label fw-semibold">
<i class="fas fa-filter me-1"></i>{% trans "Stage" %}
</label>
<select class="form-select" id="stage" name="stage">
<option value="">{% trans "All Stages" %}</option>
{% for stage_value, stage_label in stage_choices %}
<option value="{{ stage_value }}"
{% if stage_filter == stage_value %}selected{% endif %}>
{{ stage_label }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-main-action w-100">
<i class="fas fa-search me-1"></i> {% trans "Search" %}
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Results Summary -->
<div class="row mb-3">
<div class="col-md-6">
<div class="kaauh-card shadow-sm h-100">
<div class="card-body text-center">
<div class="text-info mb-2">
<i class="fas fa-users fa-2x"></i>
</div>
<h4 class="card-title">{{ total_persons }}</h4>
<p class="card-text text-muted">{% trans "Total Persons" %}</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="kaauh-card shadow-sm h-100">
<div class="card-body text-center">
<div class="text-success mb-2">
<i class="fas fa-check-circle fa-2x"></i>
</div>
<h4 class="card-title">{{ page_obj|length }}</h4>
<p class="card-text text-muted">{% trans "Showing on this page" %}</p>
</div>
</div>
</div>
</div>
<!-- Persons Table -->
<div class="kaauh-card shadow-sm">
<div class="card-body p-0">
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th scope="col">{% trans "Name" %}</th>
<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>
<th scope="col" class="text-center">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for person in page_obj %}
<tr class="person-row">
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; font-size: 14px; font-weight: 600;">
{{ person.first_name|first|upper }}{{ person.last_name|first|upper }}
</div>
<div>
<div class="fw-semibold">{{ person.first_name }} {{ person.last_name }}</div>
{% if person.address %}
<small class="text-muted">{{ person.address|truncatechars:50 }}</small>
{% endif %}
</div>
</div>
</td>
<td>
<a href="mailto:{{ person.email }}" class="text-decoration-none">
{{ person.email }}
</a>
</td>
<td>{{ person.phone|default:"-" }}</td>
<td>
<span class="badge bg-light text-dark">
{{ person.job.title|truncatechars:30 }}
</span>
</td>
<td>
{% with stage_class=person.stage|lower %}
<span class="stage-badge
{% if stage_class == 'applied' %}bg-secondary{% endif %}
{% if stage_class == 'exam' %}bg-info{% endif %}
{% if stage_class == 'interview' %}bg-warning{% endif %}
{% if stage_class == 'offer' %}bg-success{% endif %}
{% if stage_class == 'hired' %}bg-primary{% endif %}
{% if stage_class == 'rejected' %}bg-danger{% endif %}
text-white">
{{ person.get_stage_display }}
</span>
{% endwith %}
</td>
<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"
class="btn btn-sm btn-outline-secondary"
title="{% trans 'Edit Person' %}"
onclick="editPerson({{ person.id }})">
<i class="fas fa-edit"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">{% trans "No persons found" %}</h5>
<p class="text-muted">
{% if search_query or stage_filter %}
{% trans "Try adjusting your search or filter criteria." %}
{% else %}
{% trans "No persons have been added yet." %}
{% endif %}
</p>
{% if not search_query and not stage_filter and agency.assignments.exists %}
<a href="{% url 'agency_portal_submit_candidate_page' agency.assignments.first.slug %}"
class="btn btn-main-action">
<i class="fas fa-user-plus me-1"></i> {% trans "Add First Person" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="{% trans 'Persons pagination' %}" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}{% if stage_filter %}&stage={{ stage_filter }}{% endif %}">
<i class="fas fa-angle-double-left"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if stage_filter %}&stage={{ stage_filter }}{% endif %}">
<i class="fas fa-angle-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if search_query %}&q={{ search_query }}{% endif %}{% if stage_filter %}&stage={{ stage_filter }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if stage_filter %}&stage={{ stage_filter }}{% endif %}">
<i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}{% if stage_filter %}&stage={{ stage_filter }}{% endif %}">
<i class="fas fa-angle-double-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
<!-- Person Modal -->
<div class="modal fade modal-lg" id="personModal" tabindex="-1" aria-labelledby="personModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="personModalLabel">
<i class="fas fa-users me-2"></i>
{% trans "Person Details" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="personModalBody">
<form id="person_form" hx-post="{% url 'person_create' %}" hx-vals='{"view":"portal","agency":"{{ agency.slug }}"}' hx-select=".persons-list" hx-target=".persons-list" hx-swap="outerHTML"
hx-on:afterRequest="$('#personModal').modal('hide')">
{% csrf_token %}
<div class="row g-4">
<div class="col-md-4">
{{ person_form.first_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ person_form.middle_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ person_form.last_name|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
{{ person_form.email|as_crispy_field }}
</div>
<div class="col-md-6">
{{ person_form.phone|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
{{ person_form.date_of_birth|as_crispy_field }}
</div>
<div class="col-md-6">
{{ person_form.nationality|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-12">
{{ person_form.address|as_crispy_field }}
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-main-action" type="submit" form="person_form">{% trans "Save" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Close" %}
</button>
</div>
</div>
</div>
</div>
<script>
function openPersonModal(personId, personName) {
const modal = new bootstrap.Modal(document.getElementById('personModal'));
document.getElementById('person-modal-text').innerHTML = `<strong>${personName}</strong> (ID: ${personId})`;
modal.show();
}
</script>
<script>
function editPerson(personId) {
// Placeholder for edit functionality
// This would typically open a modal or navigate to edit page
console.log('Edit person:', personId);
// For now, you can redirect to a placeholder edit URL
// window.location.href = `/portal/candidates/${personId}/edit/`;
}
// Auto-submit form on filter change
document.getElementById('stage').addEventListener('change', function() {
this.form.submit();
});
// Add row click functionality
document.querySelectorAll('.person-row').forEach(row => {
row.addEventListener('click', function(e) {
// Don't trigger if clicking on buttons or links
if (e.target.closest('a, button')) {
return;
}
// Find the view details button and click it
const viewBtn = this.querySelector('a[title*="View"]');
if (viewBtn) {
viewBtn.click();
}
});
});
</script>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'agency_base.html' %}
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Submit Candidate" %} - {{ assignment.job.title }} - Agency Portal{% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load static i18n crispy_forms_tags %}
{% block title %}Create Candidate - {{ block.super }}{% endblock %}
{% block title %}Create Application - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
@ -36,7 +36,7 @@
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Outlined Button Styles */
.btn-secondary, .btn-outline-secondary {
background-color: #f8f9fa;
@ -58,7 +58,7 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Colored Header Card */
.candidate-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
@ -84,18 +84,22 @@
{% block content %}
<div class="container-fluid py-4">
<div class="card mb-4">
<div class="candidate-header-card">
<div class="d-flex justify-content-between align-items-start flex-wrap">
<div class="flex-grow-1">
<h1 class="h3 mb-1">
<i class="fas fa-user-plus"></i>
{% trans "Create New Candidate" %}
<i class="fas fa-user-plus"></i>
{% trans "Create New Application" %}
</h1>
<p class="text-white opacity-75 mb-0">{% trans "Enter details to create a new candidate record." %}</p>
<p class="text-white opacity-75 mb-0">{% trans "Enter details to create a new application record." %}</p>
</div>
<div class="d-flex gap-2 mt-1">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#personModal">
<i class="fas fa-user-plus me-1"></i>
<span class="d-none d-sm-inline">{% trans "Create New Person" %}</span>
</button>
<a href="{% url 'candidate_list' %}" class="btn btn-outline-light btn-sm" title="{% trans 'Back to List' %}">
<i class="fas fa-arrow-left"></i>
<span class="d-none d-sm-inline">{% trans "Back to List" %}</span>
@ -109,13 +113,13 @@
<div class="card-header bg-white border-bottom">
<h2 class="h5 mb-0 text-primary">
<i class="fas fa-file-alt me-1"></i>
{% trans "Candidate Information" %}
{% trans "Application Information" %}
</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{# Split form into two columns for better horizontal use #}
<div class="row g-4">
{% for field in form %}
@ -124,14 +128,69 @@
</div>
{% endfor %}
</div>
<hr class="mt-4 mb-4">
<button class="btn btn-main-action" type="submit">
<i class="fas fa-save me-1"></i>
{% trans "Create Candidate" %}
{% trans "Create Application" %}
</button>
</form>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade modal-lg" id="personModal" tabindex="-1" aria-labelledby="personModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="personModalLabel">
<i class="fas fa-question-circle me-2"></i>{% trans "Help" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="person_form" hx-post="{% url 'person_create' %}" hx-vals='{"view":"job"}' hx-target="#div_id_person" hx-select="#div_id_person" hx-swap="outerHTML">
{% csrf_token %}
<div class="row g-4">
<div class="col-md-4">
{{ person_form.first_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ person_form.middle_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ person_form.last_name|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
{{ person_form.email|as_crispy_field }}
</div>
<div class="col-md-6">
{{ person_form.phone|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
{{ person_form.date_of_birth|as_crispy_field }}
</div>
<div class="col-md-6">
{{ person_form.nationality|as_crispy_field }}
</div>
</div>
<div class="row g-4">
<div class="col-12">
{{ person_form.address|as_crispy_field }}
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-main-action" data-bs-dismiss="modal" form="person_form">{% trans "Save" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -321,6 +321,12 @@
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="documents-tab" data-bs-toggle="tab" data-bs-target="#documents-pane" type="button" role="tab" aria-controls="documents-pane" aria-selected="false">
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
</button>
</li>
</ul>
<div class="card-body">
@ -417,7 +423,7 @@
</div>
{% endif %}
{% if candidate.get_interview_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
@ -440,13 +446,13 @@
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
</small>
</div>
</div>
{% endif %}
{% if candidate.hired_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
@ -454,19 +460,25 @@
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.hired_date|date:"M d, Y" }}
</small>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{# TAB 4 CONTENT: PARSED SUMMARY #}
{# TAB 4 CONTENT: DOCUMENTS #}
<div class="tab-pane fade" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab">
{% with documents=candidate.documents %}
{% include 'includes/document_list.html' %}
{% endwith %}
</div>
{# TAB 5 CONTENT: PARSED SUMMARY #}
{% if candidate.parsed_summary %}
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
@ -666,7 +678,7 @@
<div class="card shadow-sm mb-4 p-2">
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire:" %}
{% with days=candidate.time_to_hire_days %}
{% if days > 0 %}
{{ days }} day{{ days|pluralize }}
@ -712,4 +724,4 @@
{% if user.is_staff %}
{% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %}
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -196,7 +196,7 @@
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
{% trans "Candidate List" %}
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span>
<small class="text-muted fw-normal ms-2">(Sorted by AI Score)</small>
<small class="text-muted fw-normal ms-2">({% trans "Sorted by AI Score" %})</small>
</h2>
<div class="kaauh-card shadow-sm p-3">
@ -210,7 +210,7 @@
{# Select Input Group #}
<div>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected>
----------
@ -259,7 +259,7 @@
{% endif %}
</th>
<th style="width: 15%;">{% trans "Name" %}</th>
<th style="width: 15%;">{% trans "Contact" %}</th>
<th style="width: 15%;">{% trans "Contact Info" %}</th>
<th style="width: 10%;" class="text-center">{% trans "AI Score" %}</th>
<th style="width: 15%;">{% trans "Exam Date" %}</th>
<th style="width: 10%;" class="text-center">{% trans "Exam Results" %}</th>
@ -313,7 +313,7 @@
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'exam' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
{{ candidate.interview_status }}
{{ candidate.exam_status }}
</button>
{% else %}
--

View File

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}Candidates - {{ block.super }}{% endblock %}
{% block title %}Applications - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
@ -190,13 +190,11 @@
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-users me-2"></i> {% trans "Candidate Profiles" %}
<i class="fas fa-users me-2"></i> {% trans "Applications List" %}
</h1>
{% if user.is_staff %}
<a href="{% url 'candidate_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New Candidate" %}
<i class="fas fa-plus me-1"></i> {% trans "Add New Application" %}
</a>
{% endif %}
</div>
<div class="card mb-4 shadow-sm no-hover">
@ -255,7 +253,7 @@
</div>
</div>
{% if candidates %}
{% if applications %}
<div id="candidate-list">
{# View Switcher - list_id must match the container ID #}
{% include "includes/_list_view_switcher.html" with list_id="candidate-list" %}
@ -278,7 +276,7 @@
</tr>
</thead>
<tbody>
{% for candidate in candidates %}
{% for candidate in applications %}
<tr>
<td class="fw-medium"><a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none link-secondary">{{ candidate.name }}<a></td>
<td>{{ candidate.email }}</td>
@ -307,14 +305,14 @@
</span>
</td>
<td>
{% if candidate.hiring_agency %}
{% if candidate.hiring_agency and candidate.hiring_source == 'Agency' %}
<a href="{% url 'agency_detail' candidate.hiring_agency.slug %}" class="text-decoration-none">
<span class="badge bg-info">
<span class="badge bg-primary">
<i class="fas fa-building"></i> {{ candidate.hiring_agency.name }}
</span>
</a>
{% else %}
<span class="text-muted">-</span>
<span class="badge bg-primary">{{ candidate.hiring_source }}</span>
{% endif %}
</td>
<td>{{ candidate.created_at|date:"d-m-Y" }}</td>
@ -396,11 +394,11 @@
<div class="text-center py-5 card shadow-sm">
<div class="card-body">
<i class="fas fa-users fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
<h3>{% trans "No candidate profiles found" %}</h3>
<p class="text-muted">{% trans "Create your first candidate profile or adjust your filters." %}</p>
<h3>{% trans "No application found" %}</h3>
<p class="text-muted">{% trans "Create your first application." %}</p>
{% if user.is_staff %}
<a href="{% url 'candidate_create' %}" class="btn btn-main-action mt-3">
<i class="fas fa-plus me-1"></i> {% trans "Add Candidate" %}
<i class="fas fa-plus me-1"></i> {% trans "Add Application" %}
</a>
{% endif %}
</div>

View File

@ -0,0 +1,165 @@
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Candidate Dashboard" %} - ATS{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Dashboard Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<h4 class="mb-1">
<i class="fas fa-user-tie me-2 text-primary"></i>
{% trans "Welcome" %} {{ candidate.first_name }}
</h4>
<p class="text-muted mb-0">
{% trans "Manage your applications and profile" %}
</p>
</div>
{% comment %} <div class="col-md-6 text-md-end">
<span class="badge bg-success fs-6">
<i class="fas fa-circle me-1"></i>
{% trans "Active" %}
</span>
</div> {% endcomment %}
</div>
</div>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="mb-3">
<i class="fas fa-briefcase fa-2x text-primary"></i>
</div>
<h5 class="card-title">{{ candidate.job.title|default:"No Job" }}</h5>
<p class="text-muted small mb-0">
{% trans "Applied Position" %}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="mb-3">
<i class="fas fa-tasks fa-2x text-info"></i>
</div>
<h5 class="card-title">{{ candidate.stage|default:"Applied" }}</h5>
<p class="text-muted small mb-0">
{% trans "Current Stage" %}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="mb-3">
<i class="fas fa-calendar fa-2x text-success"></i>
</div>
<h5 class="card-title">{{ candidate.created_at|date:"M d, Y" }}</h5>
<p class="text-muted small mb-0">
{% trans "Application Date" %}
</p>
</div>
</div>
</div>
</div>
<!-- Profile Information -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-user me-2"></i>
{% trans "Profile Information" %}
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
{% trans "Full Name" %}
</label>
<p class="form-control-plaintext">
{{ candidate.first_name }} {{ candidate.last_name }}
</p>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
{% trans "Email Address" %}
</label>
<p class="form-control-plaintext">
{{ candidate.email }}
</p>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
{% trans "Phone Number" %}
</label>
<p class="form-control-plaintext">
{{ candidate.phone|default:"Not provided" }}
</p>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
{% trans "Resume" %}
</label>
<p class="form-control-plaintext">
{% if candidate.resume %}
<a href="{{ candidate.resume.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-download me-1"></i>
{% trans "Download Resume" %}
</a>
{% else %}
<span class="text-muted">{% trans "No resume uploaded" %}</span>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row text-center">
<div class="col-md-4 mb-3">
<button class="btn btn-outline-primary w-100">
<i class="fas fa-edit me-2"></i>
{% trans "Edit Profile" %}
</button>
</div>
<div class="col-md-4 mb-3">
<button class="btn btn-outline-success w-100">
<i class="fas fa-file-upload me-2"></i>
{% trans "Update Resume" %}
</button>
</div>
<div class="col-md-4 mb-3">
<button class="btn btn-outline-info w-100">
<i class="fas fa-eye me-2"></i>
{% trans "View Application" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -162,6 +162,13 @@
font-size: 0.8rem !important; /* Slightly smaller font */
}
.kaats-spinner {
animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */
width: 40px; /* Standard size */
height: 40px;
display: inline-block; /* Useful for table cells */
vertical-align: middle;
}
.kaats-spinner .path {
stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */
@ -265,16 +272,16 @@
<select name="screening_rating" id="screening_rating" class="form-select form-select-sm" style="width: 120px;">
<option value="">{% trans "Any Rating" %}</option>
<option value="Highly Qualified" {% if screening_rating == "Highly Qualified" %}selected{% endif %}>
Highly Qualified
{% trans "Highly Qualified" %}
</option>
<option value="Qualified" {% if screening_rating == "Qualified" %}selected{% endif %}>
Qualified
{% trans "Qualified" %}
</option>
<option value="Partially Qualified" {% if screening_rating == "Partially Qualified" %}selected{% endif %}>
Partially Qualified
{% trans "Partially Qualified" %}
</option>
<option value="Not Qualified" {% if screening_rating == "Not Qualified" %}selected{% endif %}>
Not Qualified
{% trans "Not Qualified" %}
</option>
</select>
</div>
@ -312,7 +319,7 @@
{# Select Input Group #}
<div>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected>
----------
@ -415,7 +422,7 @@
<div class="text-nowrap">
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
<circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
</svg>
<span class="text-teal-primary">{% trans 'AI scoring..' %}</span>
@ -511,7 +518,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>

View File

@ -0,0 +1,148 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Candidate Signup" %}{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-user-plus me-2"></i>
{% trans "Candidate Signup" %}
</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.first_name.id_for_label }}" class="form-label">
{% trans "First Name" %} <span class="text-danger">*</span>
</label>
{{ form.first_name }}
{% if form.first_name.errors %}
<div class="text-danger small">
{{ form.first_name.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.last_name.id_for_label }}" class="form-label">
{% trans "Last Name" %} <span class="text-danger">*</span>
</label>
{{ form.last_name }}
{% if form.last_name.errors %}
<div class="text-danger small">
{{ form.last_name.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.middle_name.id_for_label }}" class="form-label">
{% trans "Middle Name" %}
</label>
{{ form.middle_name }}
{% if form.middle_name.errors %}
<div class="text-danger small">
{{ form.middle_name.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">
{% trans "Phone Number" %} <span class="text-danger">*</span>
</label>
{{ form.phone }}
{% if form.phone.errors %}
<div class="text-danger small">
{{ form.phone.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">
{% trans "Email Address" %} <span class="text-danger">*</span>
</label>
{{ form.email }}
{% if form.email.errors %}
<div class="text-danger small">
{{ form.email.errors.0 }}
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.password.id_for_label }}" class="form-label">
{% trans "Password" %} <span class="text-danger">*</span>
</label>
{{ form.password }}
{% if form.password.errors %}
<div class="text-danger small">
{{ form.password.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.confirm_password.id_for_label }}" class="form-label">
{% trans "Confirm Password" %} <span class="text-danger">*</span>
</label>
{{ form.confirm_password }}
{% if form.confirm_password.errors %}
<div class="text-danger small">
{{ form.confirm_password.errors.0 }}
</div>
{% endif %}
</div>
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus me-2"></i>
{% trans "Sign Up" %}
</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<small class="text-muted">
{% trans "Already have an account?" %}
<a href="{% url 'portal_login' %}" class="text-decoration-none">
{% trans "Login here" %}
</a>
</small>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,14 +1,4 @@
<td class="text-center" id="status-result-{{ candidate.pk}}">
{% if not candidate.interview_status %}
<button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'exam' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
<i class="fas fa-plus"></i>
</button>
{% else %}
{% if candidate.exam_status %}
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal"
@ -21,5 +11,4 @@
{% else %}
--
{% endif %}
{% endif %}
</td>

View File

@ -1,22 +1,22 @@
<td class="text-center" id="status-result-{{ candidate.pk}}">
<td class="text-center" id="interview-result-{{ candidate.pk}}">
{% if not candidate.interview_status %}
<button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'offer' 'passed' %}"
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'interview' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
<i class="fas fa-plus"></i>
</button>
{% else %}
{% if candidate.offer_status %}
<button type="button" class="btn btn-{% if candidate.offer_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
{% if candidate.interview_status %}
<button type="button" class="btn btn-{% if candidate.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'offer' 'passed' %}"
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'interview' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
{{ candidate.offer_status }}
{{ candidate.interview_status }}
</button>
{% else %}
--

View File

@ -0,0 +1,295 @@
{% extends 'portal_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Portal Login" %} - ATS{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
body {
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
min-height: 100vh;
}
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 0;
}
.login-card {
background: white;
border-radius: 1rem;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
border: none;
max-width: 500px;
width: 100%;
margin: 0 1rem;
}
.login-header {
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
color: white;
padding: 2rem;
border-radius: 1rem 1rem 0 0;
text-align: center;
}
.login-body {
padding: 2.5rem;
}
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.btn-login {
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
border: none;
color: white;
font-weight: 600;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 99, 110, 0.3);
}
.input-group-text {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.user-type-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.user-type-card {
border: 2px solid #e9ecef;
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.user-type-card:hover {
border-color: var(--kaauh-teal);
background-color: #f8f9fa;
}
.user-type-card.selected {
border-color: var(--kaauh-teal);
background-color: rgba(0, 99, 110, 0.1);
}
.user-type-icon {
font-size: 2rem;
color: var(--kaauh-teal);
margin-bottom: 0.5rem;
}
.alert {
border-radius: 0.5rem;
border: none;
}
</style>
{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-card">
<!-- Login Header -->
<div class="login-header">
<div class="mb-3">
<i class="fas fa-users fa-3x"></i>
</div>
<h3 class="mb-2">{% trans "Portal Login" %}</h3>
<p class="mb-0 opacity-75">
{% trans "Access your personalized dashboard" %}
</p>
</div>
<!-- Login Body -->
<div class="login-body">
<!-- Login Form -->
<form method="post" novalidate>
{% csrf_token %}
<!-- Email Field -->
<div class="mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label fw-bold">
<i class="fas fa-envelope me-2"></i>
{% trans "Email Address" %}
</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-envelope"></i>
</span>
{{ form.email }}
</div>
{% if form.email.errors %}
<div class="text-danger small mt-1">
{% for error in form.email.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<!-- Password Field -->
<div class="mb-4">
<label for="{{ form.password.id_for_label }}" class="form-label fw-bold">
<i class="fas fa-lock me-2"></i>
{% trans "Password" %}
</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-key"></i>
</span>
{{ form.password }}
</div>
{% if form.password.errors %}
<div class="text-danger small mt-1">
{% for error in form.password.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<!-- User Type Selection -->
<div class="mb-4">
<label for="{{ form.user_type.id_for_label }}" class="form-label fw-bold">
<i class="fas fa-user-tag me-2"></i>
{% trans "Select User Type" %}
</label>
{{ form.user_type }}
{% if form.user_type.errors %}
<div class="text-danger small mt-1">
{% for error in form.user_type.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<!-- Submit Button -->
<div class="d-grid">
<button type="submit" class="btn btn-login btn-lg">
<i class="fas fa-sign-in-alt me-2"></i>
{% trans "Login" %}
</button>
</div>
</form>
<!-- Help Links -->
<div class="text-center mt-4">
<small class="text-muted">
{% trans "Need help?" %}
<a href="#" class="text-decoration-none">
{% trans "Contact Support" %}
</a>
</small>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Focus on user type field
const userTypeField = document.getElementById('{{ form.user_type.id_for_label }}');
if (userTypeField) {
userTypeField.focus();
}
// Form validation
const form = document.querySelector('form');
const emailField = document.getElementById('{{ form.email.id_for_label }}');
const passwordField = document.getElementById('{{ form.password.id_for_label }}');
if (form) {
form.addEventListener('submit', function(e) {
const userType = userTypeField.value;
const email = emailField.value.trim();
const password = passwordField.value.trim();
if (!userType) {
e.preventDefault();
showError('{% trans "Please select a user type." %}');
userTypeField.focus();
return;
}
if (!email) {
e.preventDefault();
showError('{% trans "Please enter your email address." %}');
emailField.focus();
return;
}
if (!password) {
e.preventDefault();
showError('{% trans "Please enter your password." %}');
passwordField.focus();
return;
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
e.preventDefault();
showError('{% trans "Please enter a valid email address." %}');
emailField.focus();
return;
}
});
}
function showError(message) {
// Remove existing alerts
const existingAlerts = document.querySelectorAll('.alert-danger');
existingAlerts.forEach(alert => alert.remove());
// Create new alert
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Insert at the top of login body
const loginBody = document.querySelector('.login-body');
loginBody.insertBefore(alertDiv, loginBody.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
});
</script>
{% endblock %}

444
translate_all_batches.py Normal file
View File

@ -0,0 +1,444 @@
#!/usr/bin/env python3
"""
Comprehensive script to translate all remaining batch files with Arabic translations
"""
import re
import os
# Comprehensive Arabic translation dictionary
TRANSLATIONS = {
# Email and Authentication
"The date and time this notification is scheduled to be sent.": "التاريخ والوقت المحدد لإرسال هذا الإشعار.",
"Send Attempts": "محاولات الإرسال",
"Failed to start the job posting process. Please try again.": "فشل في بدء عملية نشر الوظيفة. يرجى المحاولة مرة أخرى.",
"You don't have permission to view this page.": "ليس لديك إذن لعرض هذه الصفحة.",
"Account Inactive": "الحساب غير نشط",
"Princess Nourah bint Abdulrahman University": "جامعة الأميرة نورة بنت عبدالرحمن",
"Manage your personal details and security.": "إدارة تفاصيلك الشخصية والأمان.",
"Primary": "أساسي",
"Verified": "موثق",
"Unverified": "غير موثق",
"Make Primary": "جعل أساسي",
"Remove": "إزالة",
"Add Email Address": "إضافة عنوان بريد إلكتروني",
"Hello,": "مرحباً،",
"Confirm My KAAUH ATS Email": "تأكيد بريدي الإلكتروني في نظام توظيف جامعة نورة",
"Alternatively, copy and paste this link into your browser:": "بدلاً من ذلك، انسخ والصق هذا الرابط في متصفحك:",
"Password Reset Request": "طلب إعادة تعيين كلمة المرور",
"Click Here to Reset Your Password": "اضغط هنا لإعادة تعيين كلمة المرور",
"This link is only valid for a limited time.": "هذا الرابط صالح لفترة محدودة فقط.",
"Thank you,": "شكراً لك،",
"KAAUH ATS Team": "فريق نظام توظيف جامعة نورة",
"Confirm Email Address": "تأكيد عنوان البريد الإلكتروني",
"Account Verification": "التحقق من الحساب",
"Verify your email to secure your account and unlock full features.": "تحقق من بريدك الإلكتروني لتأمين حسابك وإلغاء قفل جميع الميزات.",
"Confirm Your Email Address": "تأكيد عنوان بريدك الإلكتروني",
"Verification Failed": "فشل التحقق",
"The email confirmation link is expired or invalid.": "رابط تأكيد البريد الإلكتروني منتهي الصلاحية أو غير صالح.",
"Keep me signed in": "ابق مسجلاً للدخول",
"Return to Profile": "العودة إلى الملف الشخصي",
"Enter your e-mail address to reset your password.": "أدخل عنوان بريدك الإلكتروني لإعادة تعيين كلمة المرور.",
"Remember your password?": "تتذكر كلمة المرور؟",
"Log In": "تسجيل الدخول",
"Password Reset Sent": "تم إرسال إعادة تعيين كلمة المرور",
"Return to Login": "العودة إلى تسجيل الدخول",
"Please enter your new password below.": "يرجى إدخال كلمة المرور الجديدة أدناه.",
# Common UI Elements
"Save": "حفظ",
"Cancel": "إلغاء",
"Delete": "حذف",
"Edit": "تحرير",
"View": "عرض",
"Create": "إنشاء",
"Update": "تحديث",
"Submit": "إرسال",
"Search": "بحث",
"Filter": "تصفية",
"Sort": "ترتيب",
"Export": "تصدير",
"Import": "استيراد",
"Download": "تنزيل",
"Upload": "رفع",
"Close": "إغلاق",
"Back": "رجوع",
"Next": "التالي",
"Previous": "السابق",
"First": "الأول",
"Last": "الأخير",
"Home": "الرئيسية",
"Dashboard": "لوحة التحكم",
"Profile": "الملف الشخصي",
"Settings": "الإعدادات",
"Help": "المساعدة",
"About": "حول",
"Contact": "اتصال",
"Logout": "تسجيل الخروج",
"Login": "تسجيل الدخول",
"Register": "التسجيل",
"Sign Up": "إنشاء حساب",
"Sign In": "تسجيل الدخول",
# Status Messages
"Active": "نشط",
"Inactive": "غير نشط",
"Pending": "في الانتظار",
"Completed": "مكتمل",
"Failed": "فشل",
"Success": "نجح",
"Error": "خطأ",
"Warning": "تحذير",
"Info": "معلومات",
"Loading": "جاري التحميل",
"Processing": "جاري المعالجة",
"Ready": "جاهز",
"Not Ready": "غير جاهز",
"Available": "متاح",
"Unavailable": "غير متاح",
"Online": "متصل",
"Offline": "غير متصل",
"Connected": "متصل",
"Disconnected": "منقطع",
"Enabled": "مفعل",
"Disabled": "معطل",
"Required": "مطلوب",
"Optional": "اختياري",
"Yes": "نعم",
"No": "لا",
"True": "صحيح",
"False": "خطأ",
"On": "مفعل",
"Off": "معطل",
"Open": "مفتوح",
"Closed": "مغلق",
"Locked": "مقفل",
"Unlocked": "غير مقفل",
# Form Fields
"Name": "الاسم",
"Email": "البريد الإلكتروني",
"Phone": "الهاتف",
"Address": "العنوان",
"City": "المدينة",
"Country": "البلد",
"State": "الولاية",
"Zip Code": "الرمز البريدي",
"Password": "كلمة المرور",
"Confirm Password": "تأكيد كلمة المرور",
"Username": "اسم المستخدم",
"First Name": "الاسم الأول",
"Last Name": "اسم العائلة",
"Full Name": "الاسم الكامل",
"Company": "الشركة",
"Position": "المنصب",
"Department": "القسم",
"Title": "العنوان",
"Description": "الوصف",
"Comments": "التعليقات",
"Notes": "ملاحظات",
"Date": "التاريخ",
"Time": "الوقت",
"Start Date": "تاريخ البدء",
"End Date": "تاريخ الانتهاء",
"Created": "تم الإنشاء",
"Modified": "تم التعديل",
"Updated": "تم التحديث",
# Messages
"Please select an option": "يرجى اختيار خيار",
"This field is required": "هذا الحقل مطلوب",
"Invalid email address": "عنوان بريد إلكتروني غير صالح",
"Password must be at least 8 characters": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
"Passwords do not match": "كلمات المرور غير متطابقة",
"Email already exists": "البريد الإلكتروني موجود بالفعل",
"User not found": "المستخدم غير موجود",
"Invalid credentials": "بيانات الاعتماد غير صالحة",
"Access denied": "الوصول مرفوض",
"Permission denied": "الإذن مرفوض",
"Operation successful": "تمت العملية بنجاح",
"Operation failed": "فشلت العملية",
"Data saved successfully": "تم حفظ البيانات بنجاح",
"Data deleted successfully": "تم حذف البيانات بنجاح",
"Are you sure you want to delete this item?": "هل أنت متأكد من أنك تريد حذف هذا العنصر؟",
"This action cannot be undone": "لا يمكن التراجع عن هذا الإجراء",
# Navigation
"Menu": "القائمة",
"Home": "الرئيسية",
"Dashboard": "لوحة التحكم",
"Profile": "الملف الشخصي",
"Settings": "الإعدادات",
"Admin": "المسؤول",
"Users": "المستخدمون",
"Reports": "التقارير",
"Analytics": "التحليلات",
"Messages": "الرسائل",
"Notifications": "الإشعارات",
"Tasks": "المهام",
"Calendar": "التقويم",
"Documents": "المستندات",
"Files": "الملفات",
"Media": "الوسائط",
"Help": "المساعدة",
"Support": "الدعم",
"FAQ": "الأسئلة الشائعة",
"Terms": "الشروط",
"Privacy": "الخصوصية",
"Legal": "قانوني",
# Common Actions
"Add": "إضافة",
"Remove": "إزالة",
"Edit": "تحرير",
"Update": "تحديث",
"Delete": "حذف",
"View": "عرض",
"Show": "إظهار",
"Hide": "إخفاء",
"Enable": "تفعيل",
"Disable": "تعطيل",
"Activate": "تنشيط",
"Deactivate": "إلغاء تنشيط",
"Approve": "موافقة",
"Reject": "رفض",
"Accept": "قبول",
"Decline": "رفض",
"Send": "إرسال",
"Receive": "استلام",
"Download": "تنزيل",
"Upload": "رفع",
"Import": "استيراد",
"Export": "تصدير",
"Print": "طباعة",
"Copy": "نسخ",
"Move": "نقل",
"Rename": "إعادة تسمية",
"Share": "مشاركة",
"Subscribe": "اشتراك",
"Unsubscribe": "إلغاء الاشتراك",
"Follow": "متابعة",
"Unfollow": "إلغاء المتابعة",
"Like": "إعجاب",
"Unlike": "إلغاء الإعجاب",
"Comment": "تعليق",
"Rate": "تقييم",
"Review": "مراجعة",
"Bookmark": "إشارة مرجعية",
"Favorite": "مفضل",
"Archive": "أرشفة",
"Restore": "استعادة",
"Backup": "نسخ احتياطي",
"Recover": "استرداد",
"Reset": "إعادة تعيين",
"Refresh": "تحديث",
"Reload": "إعادة تحميل",
"Sync": "مزامنة",
"Connect": "اتصال",
"Disconnect": "قطع الاتصال",
"Link": "ربط",
"Unlink": "فك الربط",
"Attach": "إرفاق",
"Detach": "فصل",
"Merge": "دمج",
"Split": "تقسيم",
"Combine": "دمج",
"Separate": "فصل",
"Group": "تجميع",
"Ungroup": "فك التجميع",
"Sort": "ترتيب",
"Filter": "تصفية",
"Search": "بحث",
"Find": "بحث",
"Replace": "استبدال",
"Clear": "مسح",
"Clean": "تنظيف",
"Empty": "فارغ",
"Full": "ممتلئ",
"All": "الكل",
"None": "لا شيء",
"Some": "بعض",
"Any": "أي",
"Other": "آخر",
"More": "المزيد",
"Less": "أقل",
"New": "جديد",
"Old": "قديم",
"Recent": "الحديث",
"Latest": "الأحدث",
"Previous": "السابق",
"Next": "التالي",
"First": "الأول",
"Last": "الأخير",
"Current": "الحالي",
"Today": "اليوم",
"Yesterday": "أمس",
"Tomorrow": "غداً",
"This week": "هذا الأسبوع",
"Last week": "الأسبوع الماضي",
"Next week": "الأسبوع القادم",
"This month": "هذا الشهر",
"Last month": "الشهر الماضي",
"Next month": "الشهر القادم",
"This year": "هذا العام",
"Last year": "العام الماضي",
"Next year": "العام القادم",
}
def translate_batch_file(batch_file_path):
"""
Translate a single batch file and return the translations
"""
translations = {}
with open(batch_file_path, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('msgid: "'):
# Extract msgid content, removing the extra quote at the beginning
msgid = line[8:-1] # Extract msgid content (skip the extra quote)
# Skip empty msgid or already Arabic text
if not msgid or msgid.strip() == "" or is_arabic_text(msgid):
i += 1
continue
# Find translation
translation = TRANSLATIONS.get(msgid, "")
if translation:
translations[msgid] = translation
print(f"✓ Found translation: '{msgid}' -> '{translation}'")
else:
print(f"✗ No translation found: '{msgid}'")
i += 1
return translations
def is_arabic_text(text):
"""Check if text contains Arabic characters"""
arabic_chars = set('ابتثجحخدذرزسشصضطظعغفقكلمنهويءآأؤإئابةة')
return any(char in arabic_chars for char in text)
def process_all_batches():
"""
Process all batch files and create a comprehensive translation file
"""
all_translations = {}
# Process batches 02-35 (batch 01 already done)
for batch_num in range(2, 36):
batch_file = f"translation_batch_{batch_num:02d}.txt"
if os.path.exists(batch_file):
print(f"\n=== Processing {batch_file} ===")
batch_translations = translate_batch_file(batch_file)
all_translations.update(batch_translations)
print(f"Found {len(batch_translations)} translations in {batch_file}")
else:
print(f"⚠️ {batch_file} not found")
return all_translations
def create_translation_script(all_translations):
"""
Create a script to apply all translations to the main django.po file
"""
script_content = '''#!/usr/bin/env python3
"""
Script to apply all batch translations to the main django.po file
"""
import re
def apply_all_translations():
"""
Apply all translations to the main django.po file
"""
# All translations from batches 02-35
translations = {
'''
for english, arabic in all_translations.items():
script_content += f' "{english}": "{arabic}",\n'
script_content += ''' }
main_po_file = "locale/ar/LC_MESSAGES/django.po"
# Read the main django.po file
with open(main_po_file, 'r', encoding='utf-8') as f:
main_content = f.read()
# Apply translations to main file
updated_content = main_content
applied_count = 0
for english, arabic in translations.items():
# Pattern to find msgid followed by empty msgstr
pattern = rf'(msgid "{re.escape(english)}"\\s*\\nmsgstr) ""'
replacement = rf'\\1 "{arabic}"'
if re.search(pattern, updated_content):
updated_content = re.sub(pattern, replacement, updated_content)
applied_count += 1
print(f"✓ Applied: '{english}' -> '{arabic}'")
else:
print(f"✗ Not found: '{english}'")
# Write updated content back to main file
with open(main_po_file, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f"\\nApplied {applied_count} translations to {main_po_file}")
return applied_count
def main():
"""Main function to apply all translations"""
print("Applying all batch translations to main django.po file...")
applied_count = apply_all_translations()
if applied_count > 0:
print(f"\\n✅ Successfully applied {applied_count} translations!")
print("Next steps:")
print("1. Run: python manage.py compilemessages")
print("2. Test the translations in the application")
else:
print("\\n❌ No translations were applied.")
if __name__ == "__main__":
main()
'''
with open("apply_all_translations.py", 'w', encoding='utf-8') as f:
f.write(script_content)
print("Created apply_all_translations.py script")
def main():
"""Main function to process all batches"""
print("🚀 Starting comprehensive translation process...")
# Process all batch files
all_translations = process_all_batches()
print(f"\n📊 Summary:")
print(f"Total translations found: {len(all_translations)}")
if all_translations:
# Create the application script
create_translation_script(all_translations)
print(f"\n✅ Translation processing complete!")
print(f"📝 Created apply_all_translations.py with {len(all_translations)} translations")
print(f"\n🎯 Next steps:")
print(f"1. Run: python apply_all_translations.py")
print(f"2. Run: python manage.py compilemessages")
print(f"3. Test the translations in the application")
else:
print("\n❌ No translations found to process.")
if __name__ == "__main__":
main()

58
translate_batch_01.py Normal file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
Script to add Arabic translations for batch 01
"""
# Arabic translations for batch 01
translations = {
"": "", # Line 7 - empty string, keep as is
"Website": "الموقع الإلكتروني",
"Admin Notes": "ملاحظات المسؤول",
"Save Assignment": "حفظ التكليف",
"Assignment": "التكليف",
"Expires At": "ينتهي في",
"Access Token": "رمز الوصول",
"Subject": "الموضوع",
"Recipients": "المستلمون",
"Internal staff involved in the recruitment process for this job": "الموظفون الداخليون المشاركون في عملية التوظيف لهذه الوظيفة",
"External Participant": "مشارك خارجي",
"External participants involved in the recruitment process for this job": "المشاركون الخارجيون المشاركون في عملية التوظيف لهذه الوظيفة",
"Reason for canceling the job posting": "سبب إلغاء نشر الوظيفة",
"Name of person who cancelled this job": "اسم الشخص الذي ألغى هذه الوظيفة",
"Hired": "تم التوظيف",
"Author": "المؤلف",
"Endpoint URL for sending candidate data (for outbound sync)": "عنوان URL لنقطة النهاية لإرسال بيانات المرشح (للمزامنة الصادرة)",
"HTTP method for outbound sync requests": "طريقة HTTP لطلبات المزامنة الصادرة",
"HTTP method for connection testing": "طريقة HTTP لاختبار الاتصال",
"Custom Headers": "رؤوس مخصصة",
"JSON object with custom HTTP headers for sync requests": "كائن JSON يحتوي على رؤوس HTTP مخصصة لطلبات المزامنة",
"Supports Outbound Sync": "يدعم المزامنة الصادرة",
"Whether this source supports receiving candidate data from ATS": "ما إذا كان هذا المصدر يدعم استقبال بيانات المرشح من نظام تتبع المتقدمين",
"Expired": "منتهي الصلاحية",
"Maximum candidates agency can submit for this job": "الحد الأقصى للمرشحين الذين يمكن للوكالة تقديمهم لهذه الوظيفة"
}
def update_batch_file():
"""Update the batch file with Arabic translations"""
input_file = "translation_batch_01.txt"
output_file = "translation_batch_01_completed.txt"
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
# Replace empty msgstr with translations
for english, arabic in translations.items():
if english: # Skip empty string
# Find the pattern and replace
old_pattern = f'msgid: "{english}"\nmsgstr: ""\n\nArabic Translation: \nmsgstr: ""'
new_pattern = f'msgid: "{english}"\nmsgstr: ""\n\nArabic Translation: \nmsgstr: "{arabic}"'
content = content.replace(old_pattern, new_pattern)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated batch file saved as: {output_file}")
print("Arabic translations added for batch 01")
if __name__ == "__main__":
update_batch_file()

204
translation_batch_01.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 01 ===
Translations 1-25 of 843
============================================================
Line 7:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1041:
msgid: "Number of candidates submitted so far"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1052:
msgid: "Deadline for agency to submit candidates"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1068:
msgid: "Original deadline before extensions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1078:
msgid: "Agency Job Assignment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1082:
msgid: "Agency Job Assignments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1086:
msgid: "Deadline date must be in the future"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1090:
msgid: "Maximum candidates must be greater than 0"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1094:
msgid: "Candidates submitted cannot exceed maximum candidates"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1098:
msgid: "Unique Token"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1108:
msgid: "Password for agency access"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1119:
msgid: "When this access link expires"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1124:
msgid: "Last Accessed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1128:
msgid: "Access Count"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1132:
msgid: "Agency Access Link"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1136:
msgid: "Agency Access Links"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1140:
msgid: "Expiration date must be in the future"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1190:
msgid: "In-App"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1194:
msgid: "Pending"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1198:
msgid: "Sent"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1202:
msgid: "Read"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1206:
msgid: "Retrying"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1210:
msgid: "Recipient"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1214:
msgid: "Notification Message"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1218:
msgid: "Notification Type"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 01 ===
Translations 1-25 of 867
============================================================
Line 7:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 320:
msgid: "Website"
msgstr: ""
Arabic Translation:
msgstr: "الموقع الإلكتروني"
----------------------------------------
Line 406:
msgid: "Admin Notes"
msgstr: ""
Arabic Translation:
msgstr: "ملاحظات المسؤول"
----------------------------------------
Line 410:
msgid: "Save Assignment"
msgstr: ""
Arabic Translation:
msgstr: "حفظ التكليف"
----------------------------------------
Line 416:
msgid: "Assignment"
msgstr: ""
Arabic Translation:
msgstr: "التكليف"
----------------------------------------
Line 422:
msgid: "Expires At"
msgstr: ""
Arabic Translation:
msgstr: "ينتهي في"
----------------------------------------
Line 449:
msgid: "Access Token"
msgstr: ""
Arabic Translation:
msgstr: "رمز الوصول"
----------------------------------------
Line 474:
msgid: "Subject"
msgstr: ""
Arabic Translation:
msgstr: "الموضوع"
----------------------------------------
Line 485:
msgid: "Recipients"
msgstr: ""
Arabic Translation:
msgstr: "المستلمون"
----------------------------------------
Line 525:
msgid: "Internal staff involved in the recruitment process for this job"
msgstr: ""
Arabic Translation:
msgstr: "الموظفون الداخليون المشاركون في عملية التوظيف لهذه الوظيفة"
----------------------------------------
Line 529:
msgid: "External Participant"
msgstr: ""
Arabic Translation:
msgstr: "مشارك خارجي"
----------------------------------------
Line 533:
msgid: "External participants involved in the recruitment process for this job"
msgstr: ""
Arabic Translation:
msgstr: "المشاركون الخارجيون المشاركون في عملية التوظيف لهذه الوظيفة"
----------------------------------------
Line 541:
msgid: "Reason for canceling the job posting"
msgstr: ""
Arabic Translation:
msgstr: "سبب إلغاء نشر الوظيفة"
----------------------------------------
Line 551:
msgid: "Name of person who cancelled this job"
msgstr: ""
Arabic Translation:
msgstr: "اسم الشخص الذي ألغى هذه الوظيفة"
----------------------------------------
Line 595:
msgid: "Hired"
msgstr: ""
Arabic Translation:
msgstr: "تم التوظيف"
----------------------------------------
Line 782:
msgid: "Author"
msgstr: ""
Arabic Translation:
msgstr: "المؤلف"
----------------------------------------
Line 877:
msgid: "Endpoint URL for sending candidate data (for outbound sync)"
msgstr: ""
Arabic Translation:
msgstr: "عنوان URL لنقطة النهاية لإرسال بيانات المرشح (للمزامنة الصادرة)"
----------------------------------------
Line 887:
msgid: "HTTP method for outbound sync requests"
msgstr: ""
Arabic Translation:
msgstr: "طريقة HTTP لطلبات المزامنة الصادرة"
----------------------------------------
Line 897:
msgid: "HTTP method for connection testing"
msgstr: ""
Arabic Translation:
msgstr: "طريقة HTTP لاختبار الاتصال"
----------------------------------------
Line 901:
msgid: "Custom Headers"
msgstr: ""
Arabic Translation:
msgstr: "رؤوس مخصصة"
----------------------------------------
Line 905:
msgid: "JSON object with custom HTTP headers for sync requests"
msgstr: ""
Arabic Translation:
msgstr: "كائن JSON يحتوي على رؤوس HTTP مخصصة لطلبات المزامنة"
----------------------------------------
Line 909:
msgid: "Supports Outbound Sync"
msgstr: ""
Arabic Translation:
msgstr: "يدعم المزامنة الصادرة"
----------------------------------------
Line 913:
msgid: "Whether this source supports receiving candidate data from ATS"
msgstr: ""
Arabic Translation:
msgstr: "ما إذا كان هذا المصدر يدعم استقبال بيانات المرشح من نظام تتبع المتقدمين"
----------------------------------------
Line 1026:
msgid: "Expired"
msgstr: ""
Arabic Translation:
msgstr: "منتهي الصلاحية"
----------------------------------------
Line 1030:
msgid: "Maximum candidates agency can submit for this job"
msgstr: ""
Arabic Translation:
msgstr: "الحد الأقصى للمرشحين الذين يمكن للوكالة تقديمهم لهذه الوظيفة"
----------------------------------------

204
translation_batch_02.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 02 ===
Translations 26-50 of 843
============================================================
Line 1234:
msgid: "The date and time this notification is scheduled to be sent."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1238:
msgid: "Send Attempts"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1275:
msgid: "Failed to start the job posting process. Please try again."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1291:
msgid: "Model Changes (CRUD)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1295:
msgid: "You don't have permission to view this page."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1300:
msgid: "Account Inactive"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1307:
msgid: "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1314:
msgid: "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1321:
msgid: "Princess Nourah bint Abdulrahman University"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1334:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1339:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1367:
msgid: "Manage your personal details and security."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1399:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1405:
msgid: "Primary"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1409:
msgid: "Verified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1413:
msgid: "Unverified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1417:
msgid: "Make Primary"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1428:
msgid: "Remove"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1438:
msgid: "Add Email Address"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1451:
msgid: "Hello,"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1456:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1463:
msgid: "Confirm My KAAUH ATS Email"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1468:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1474:
msgid: "Alternatively, copy and paste this link into your browser:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1479:
msgid: "Password Reset Request"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_03.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 03 ===
Translations 51-75 of 843
============================================================
Line 1484:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1491:
msgid: "Click Here to Reset Your Password"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1496:
msgid: "This link is only valid for a limited time."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1501:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1508:
msgid: "Thank you,"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1513:
msgid: "KAAUH ATS Team"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1518:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1524:
msgid: "Confirm Email Address"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1528:
msgid: "Account Verification"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1532:
msgid: "Verify your email to secure your account and unlock full features."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1536:
msgid: "Confirm Your Email Address"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1541:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1553:
msgid: "Verification Failed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1557:
msgid: "The email confirmation link is expired or invalid."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1561:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1603:
msgid: "Keep me signed in"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1649:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1654:
msgid: "Return to Profile"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1658:
msgid: "Enter your e-mail address to reset your password."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1674:
msgid: "Remember your password?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1678:
msgid: "Log In"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1683:
msgid: "Password Reset Sent"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1695:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1701:
msgid: "Return to Login"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1712:
msgid: "Please enter your new password below."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_04.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 04 ===
Translations 76-100 of 843
============================================================
Line 1716:
msgid: "You can then log in."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1720:
msgid: "Password Reset Failed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1724:
msgid: "The password reset link is invalid or has expired."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1728:
msgid: "Request New Reset Link"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1744:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1756:
msgid: "Verify Your Email Address"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1768:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1774:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1780:
msgid: "Change or Resend Email"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1789:
msgid: "Django site admin"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1799:
msgid: "KAAUH Agency Portal"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1807:
msgid: "kaauh logo green bg"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1827:
msgid: "Logout"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1941:
msgid: "Ready to Apply?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1945:
msgid: "Review the job details, then apply below."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1950:
msgid: "Apply for this Position"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1969:
msgid: "Not specified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 1992:
msgid: "JOB ID:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2062:
msgid: "Submission Metadata"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2066:
msgid: "Submission ID:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2077:
msgid: "Form:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2099:
msgid: "Field Property"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2133:
msgid: "Field Required"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2141:
msgid: "Yes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2149:
msgid: "No"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_05.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 05 ===
Translations 101-125 of 843
============================================================
Line 2153:
msgid: "No response fields were found for this submission."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2157:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2180:
msgid: "Submissions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2184:
msgid: "All Submissions Table"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2188:
msgid: "All Submissions for"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2193:
msgid: "Submission ID"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2232:
msgid: "Page"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2238:
msgid: "of"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2250:
msgid: "There are no submissions for this form template yet."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2254:
msgid: "Submissions for"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2460:
msgid: "Careers"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2473:
msgid: "AI Score"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2484:
msgid: "Top Keywords"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2488:
msgid: "Experience"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2492:
msgid: "years"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2496:
msgid: "Recent Role:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2500:
msgid: "Skills"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2504:
msgid: "Soft Skills:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2508:
msgid: "Industry Match:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2513:
msgid: "Recommendation"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2518:
msgid: "Strengths"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2523:
msgid: "Weaknesses"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2528:
msgid: "Criteria Assessment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2541:
msgid: "Met"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2547:
msgid: "Not Met"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_06.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 06 ===
Translations 126-150 of 843
============================================================
Line 2558:
msgid: "Screening Rating"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2563:
msgid: "Language Fluency"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2569:
msgid: "Success"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2574:
msgid: "Copied \"%(text)s\" to clipboard!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2584:
msgid: "System Audit Logs"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2588:
msgid: "Viewing Logs"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2592:
msgid: "Displaying"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2596:
msgid: "total records."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2614:
msgid: "User"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2618:
msgid: "Model"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2622:
msgid: "Object PK"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2648:
msgid: "Path"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2652:
msgid: "CREATE"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2656:
msgid: "UPDATE"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2660:
msgid: "DELETE"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2664:
msgid: "Login"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2674:
msgid: "No logs found for this section or the database is empty."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2685:
msgid: "Email will be sent to all selected recipients"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2700:
msgid: "Loading..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2704:
msgid: "Sending email..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2708:
msgid: "Sending..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2727:
msgid: "Meeting Details (will appear after scheduling):"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2743:
msgid: "Click here to join meeting"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2772:
msgid: "Processing..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2776:
msgid: "An unknown error occurred."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_07.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 07 ===
Translations 151-175 of 843
============================================================
Line 2780:
msgid: "An error occurred while processing your request."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2788:
msgid: "Bulk Interview Scheduling"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2792:
msgid: "Configure time slots for:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2813:
msgid: "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2868:
msgid: "Thank You!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2872:
msgid: "Your application has been submitted successfully"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2876:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2883:
msgid: "Return to Job Listings"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2887:
msgid: "Job ID#"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 2919:
msgid: "Link"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3005:
msgid: "Hashtags (For Promotion/Search on Linkedin)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3065:
msgid: "Search by name, email, phone, or stage..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3069:
msgid: "Filter Results"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3074:
msgid: "Clear Filters"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3167:
msgid: "JOB ID: "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3171:
msgid: "Share Public Link"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3177:
msgid: "Copied!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3215:
msgid: "Tracking"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3249:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3269:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3330:
msgid: "Candidate Categories & Scores"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3334:
msgid: "Key Performance Indicators"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3338:
msgid: "Avg. AI Score"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3343:
msgid: "High Potential"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3353:
msgid: "Avg. Exam Review"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_08.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 08 ===
Translations 176-200 of 843
============================================================
Line 3357:
msgid: "Vacancy Fill Rate"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3373:
msgid: "Status form not available. Please check your view."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3380:
msgid: "Save Changes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3396:
msgid: "Search by Title or Department"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3425:
msgid: "Archived"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3440:
msgid: "Clear"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3450:
msgid: "Max Apps"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3481:
msgid: "All"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3486:
msgid: "Screened"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3496:
msgid: "Form"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3500:
msgid: "N/A"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3520:
msgid: "Create your first job posting to get started or adjust your filters."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3554:
msgid: "Search by Topic"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3596:
msgid: "Create your first meeting or adjust your filters."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3641:
msgid: "minutes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3655:
msgid: "Assigned Participants"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3665:
msgid: "External Participants"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3669:
msgid: "System User"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3673:
msgid: "Comments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3696:
msgid: "No comments yet. Be the first to comment!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3712:
msgid: "You must be logged in to add a comment."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3730:
msgid: "You are updating the existing meeting schedule."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3748:
msgid: "Candidate has upcoming interviews. Updating existing schedule."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3752:
msgid: "e.g., Technical Screening, HR Interview"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3762:
msgid: "Save"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_09.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 09 ===
Translations 201-225 of 843
============================================================
Line 3858:
msgid: "This participant is not currently assigned to any job."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3862:
msgid: "Metadata"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3873:
msgid: "at"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3877:
msgid: "Total Assigned Jobs"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3896:
msgid: "This action cannot be undone."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3913:
msgid: "Search by Name or Email"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3917:
msgid: "Filter by Assigned Job"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3935:
msgid: "Create your first participant record or adjust your filters."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3952:
msgid: "Secure access link for agency candidate submissions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3984:
msgid: "Access Credentials"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 3996:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4002:
msgid: "Usage Statistics"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4006:
msgid: "Total Accesses"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4010:
msgid: "Never"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4014:
msgid: "View Assignment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4032:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4045:
msgid: "Generate a secure access link for agency to submit candidates"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4057:
msgid: "Select the agency job assignment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4061:
msgid: "When will this access link expire?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4065:
msgid: "Max Submissions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4069:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4074:
msgid: "Whether this access link is currently active"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4078:
msgid: "Notes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4088:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4094:
msgid: "Assignment Details and Management"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_10.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 10 ===
Translations 226-250 of 843
============================================================
Line 4099:
msgid: "Edit Assignment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4166:
msgid: "Submission Progress"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4194:
msgid: "Recent Messages"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4201:
msgid: "From"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4207:
msgid: "New"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4218:
msgid: "Extend Assignment Deadline"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4234:
msgid: "Token copied to clipboard!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4238:
msgid: "Assign a job to an external hiring agency"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4242:
msgid: "Maximum number of candidates the agency can submit"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4246:
msgid: "Date and time when submission period ends"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4263:
msgid: "Total Assignments:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4267:
msgid: "New Assignment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4271:
msgid: "Search by agency or job title..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4281:
msgid: "Assignments pagination"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4291:
msgid: "Create your first agency assignment to get started."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4295:
msgid: "Create Assignment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4306:
msgid: "You are about to delete a hiring agency. This action cannot be undone."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4316:
msgid: "Warning: This action cannot be undone!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4320:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4326:
msgid: "Agency to be Deleted"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4336:
msgid: "candidate(s) are associated with this agency."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4340:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4346:
msgid: "What will happen when you delete this agency?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4350:
msgid: "The agency profile and all its information will be permanently deleted"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4360:
msgid: "Associated candidates will lose their agency reference"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_11.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 11 ===
Translations 251-275 of 843
============================================================
Line 4364:
msgid: "Historical data linking candidates to this agency will be lost"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4368:
msgid: "This action cannot be undone under any circumstances"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4372:
msgid: "Type the agency name to confirm deletion:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4376:
msgid: "This is required to prevent accidental deletions."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4380:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4386:
msgid: "Delete Agency Permanently"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4404:
msgid: "Hiring Agency Details and Candidate Management"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4408:
msgid: "Assign job"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4467:
msgid: "This agency hasn't submitted any candidates yet."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4477:
msgid: "Total"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4487:
msgid: "Visit Website"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4509:
msgid: "Update the hiring agency information below."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4513:
msgid: "Fill in the details to add a new hiring agency."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4517:
msgid: "Please correct the errors below:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4531:
msgid: "Quick Tips"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4535:
msgid: "Provide accurate contact information for better communication"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4539:
msgid: "Include a valid website URL if available"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4543:
msgid: "Add a detailed description to help identify the agency"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4547:
msgid: "All fields marked with * are required"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4575:
msgid: "Search by name, contact person, email, or country..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4579:
msgid: "Agency pagination"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4589:
msgid: "No hiring agencies have been added yet."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4593:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4644:
msgid: "Submit candidates using the form above to get started."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4662:
msgid: "Assignment Info"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_12.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 12 ===
Translations 276-300 of 843
============================================================
Line 4666:
msgid: "Days Remaining"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4671:
msgid: "days"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4675:
msgid: "Submission Rate"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4679:
msgid: "Send Message to Admin"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4683:
msgid: "Priority"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4687:
msgid: "Low"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4691:
msgid: "Medium"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4695:
msgid: "High"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4718:
msgid: "Error loading candidate data. Please try again."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4723:
msgid: "Error updating candidate. Please try again."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4728:
msgid: "Error removing candidate. Please try again."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4739:
msgid: "Welcome back"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4743:
msgid: "Total Assignments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4747:
msgid: "Active Assignments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4764:
msgid: "Your Job Assignments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4768:
msgid: "assignments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4772:
msgid: "days left"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4776:
msgid: "days overdue"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4780:
msgid: "Submissions Closed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4784:
msgid: "No Job Assignments Found"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4788:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4794:
msgid: "Agency Portal Login"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4804:
msgid: "Enter the access token provided by the hiring organization"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4808:
msgid: "Enter the password for this access token"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4812:
msgid: "Access Portal"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_13.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 13 ===
Translations 301-325 of 843
============================================================
Line 4816:
msgid: "Need Help?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4826:
msgid: "Reach out to your hiring contact"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4836:
msgid: "View user guides and tutorials"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4840:
msgid: "Security Notice"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4844:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4850:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4856:
msgid: "Please enter your access token."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4860:
msgid: "Please enter your password."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4876:
msgid: "Days Remaining:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4927:
msgid: "Click to upload or drag and drop"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4931:
msgid: "Accepted formats: PDF, DOC, DOCX (Maximum 5MB)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4935:
msgid: "Remove File"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4939:
msgid: "Additional Notes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4943:
msgid: "Notes (Optional)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4947:
msgid: "Any additional information about the candidate"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4951:
msgid: "Submitted candidates will be reviewed by the hiring team."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4961:
msgid: "This assignment has expired. Submissions are no longer accepted."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4965:
msgid: "Maximum candidate limit reached for this assignment."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4969:
msgid: "This assignment is not currently active."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4973:
msgid: "Submitting candidate..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4977:
msgid: "Please wait while we process your submission."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4981:
msgid: "Please upload a PDF, DOC, or DOCX file."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 4985:
msgid: "File size must be less than 5MB."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5001:
msgid: "Error submitting candidate. Please try again."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5005:
msgid: "Network error. Please check your connection and try again."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_14.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 14 ===
Translations 326-350 of 843
============================================================
Line 5041:
msgid: "Journey Timeline"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5087:
msgid: "AI Analysis Report"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5091:
msgid: "Match Score"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5095:
msgid: "Category"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5099:
msgid: "Job Fit Narrative"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5109:
msgid: "Years of Experience:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5113:
msgid: "Most Recent Job Title:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5117:
msgid: "Experience Industry Match:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5121:
msgid: "Soft Skills Score:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5131:
msgid: "Minimum Requirements Met:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5135:
msgid: "Screening Stage Rating:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5139:
msgid: "Resume is being parsed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5143:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5175:
msgid: "View Resume AI Overview"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5179:
msgid: "Time to Hire: "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5183:
msgid: "Unable to Parse Resume , click to retry"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5193:
msgid: "Candidates in Exam Stage:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5197:
msgid: "Export exam candidates to CSV"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5205:
msgid: "Export CSV"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5222:
msgid: "Screening Stage"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5232:
msgid: "No candidates are currently in the Exam stage for this job."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5236:
msgid: "Candidate Details & Exam Update"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5253:
msgid: "Successfully Hired:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5257:
msgid: "Sync hired candidates to external sources"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5268:
msgid: "Export hired candidates to CSV"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_15.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 15 ===
Translations 351-375 of 843
============================================================
Line 5272:
msgid: "Congratulations!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5276:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5303:
msgid: "Loading content..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5313:
msgid: "Syncing candidates..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5317:
msgid: "Syncing hired candidates..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5321:
msgid: "Please wait while we sync candidates to external sources."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5325:
msgid: "Syncing..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5329:
msgid: "An unexpected error occurred during sync."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5345:
msgid: "Successful:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5387:
msgid: "Sync task failed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5391:
msgid: "Failed to check sync status"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5395:
msgid: "Sync timed out after 5 minutes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5399:
msgid: "Sync in progress..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5415:
msgid: "Candidates in Interview Stage:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5419:
msgid: "Export interview candidates to CSV"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5465:
msgid: "Minutes"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5469:
msgid: "No candidates are currently in the Interview stage for this job."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5475:
msgid: "Candidate Details / Bulk Action Form"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5479:
msgid: "Manage all participants"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5484:
msgid: "Users"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5488:
msgid: "Loading email form..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5500:
msgid: "Filter by Job"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5504:
msgid: "Major"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5538:
msgid: "Candidates in Offer Stage:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5542:
msgid: "Export offer candidates to CSV"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_16.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 16 ===
Translations 376-400 of 843
============================================================
Line 5546:
msgid: "To Hired"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5556:
msgid: "No candidates are currently in the Offer stage for this job."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5566:
msgid: "Job:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5570:
msgid: "Export screening candidates to CSV"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5574:
msgid: "AI Scoring & Top Candidate Filter"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5578:
msgid: "Min AI Score"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5582:
msgid: "Min Years Exp"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5586:
msgid: "Any Rating"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5614:
msgid: "Is Qualified?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5618:
msgid: "Professional Category"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5622:
msgid: "Top 3 Skills"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5626:
msgid: "AI scoring.."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5630:
msgid: "No candidates match the current stage and filter criteria."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5666:
msgid: "Recruitment Analytics"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5670:
msgid: "Data Scope: "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5674:
msgid: "Data Scope: All Jobs"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5684:
msgid: "All Jobs (Default View)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5688:
msgid: "Daily Candidate Applications Trend"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5698:
msgid: "Pipeline Funnel: "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5702:
msgid: "Total Pipeline Funnel (All Jobs)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5706:
msgid: "Time-to-Hire Target Check"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5716:
msgid: "Top 5 Most Applied Jobs"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5738:
msgid: "Daily Applications (Last 30 Days)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5754:
msgid: "Mark All as Read"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5758:
msgid: "What this will do"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_17.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 17 ===
Translations 401-425 of 843
============================================================
Line 5781:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5787:
msgid: "All caught up!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5791:
msgid: "You don't have any unread notifications to mark as read."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5795:
msgid: "Yes, Mark All as Read"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5812:
msgid: "Notification Preview"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5829:
msgid: "View notification details and manage your preferences"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5834:
msgid: "Mark as Read"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5839:
msgid: "Mark as Unread"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5867:
msgid: "Delivery Attempts"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5919:
msgid: "Mark All Read"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5930:
msgid: "Unread"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5934:
msgid: "All Types"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5938:
msgid: "Filter"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5942:
msgid: "Total Notifications"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5946:
msgid: "Email Notifications"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5956:
msgid: "Mark as read"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5960:
msgid: "Mark as unread"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5970:
msgid: "Notifications pagination"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5980:
msgid: "Try adjusting your filters to see more notifications."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5990:
msgid: "Name / Contact"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 5994:
msgid: "View Details and Score Breakdown"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6004:
msgid: "Move to Next Stage"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6008:
msgid: "Move to"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6024:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6031:
msgid: "Days"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_18.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 18 ===
Translations 426-450 of 843
============================================================
Line 6035:
msgid: "Target:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6039:
msgid: "Max Scale:"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6049:
msgid: "Home"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6065:
msgid: "All Active & Drafted Positions (Global)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6073:
msgid: "Currently Open Requisitions (Scoped)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6077:
msgid: "Total Profiles in Current Scope"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6081:
msgid: "Total Slots to be Filled (Scoped)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6091:
msgid: "Total Recruiters/Interviewers (Global)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6101:
msgid: "Total Job Posts Sent to LinkedIn (Global)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6105:
msgid: "New Apps (7 Days)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6109:
msgid: "Incoming applications last week"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6113:
msgid: "Avg. Apps per Job"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6117:
msgid: "Average Applications per Job (Scoped)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6127:
msgid: "Avg. Days (Application to Hired)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6131:
msgid: "Avg. Match Score"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6135:
msgid: "Average AI Score (Current Scope)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6140:
msgid: "Score ≥ 75%% Profiles"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6150:
msgid: "Scheduled Interviews (Current Week)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6154:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6166:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6172:
msgid: "Please select a date and time for the interview."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6193:
msgid: "Search by Title or Creator"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6253:
msgid: "Admin Settings Dashboard"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6257:
msgid: "Staff User List"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6273:
msgid: "Last Login"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_19.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 19 ===
Translations 451-475 of 843
============================================================
Line 6277:
msgid: "Deactivate User"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6318:
msgid: "Manage email addresses"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6322:
msgid: "Security"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6338:
msgid: "Username"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6342:
msgid: "Date Joined"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6347:
msgid: "{editor}: Editing failed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6352:
msgid: "{editor}: Editing failed: {e}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6358:
msgid: "{text} {deprecated_message}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6381:
msgid: "DeprecationWarning: The command {name!r} is deprecated.{extra_message}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6386:
msgid: "Aborted!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6391:
msgid: "Commands"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6395:
msgid: "Missing command."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6399:
msgid: "No such command {name!r}."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6403:
msgid: "Value must be an iterable."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6418:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6425:
msgid: "env var: {var}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6432:
msgid: "default: {default}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6437:
msgid: "(dynamic)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6442:
msgid: "%(prog)s, version %(version)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6446:
msgid: "Show the version and exit."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6450:
msgid: "Show this message and exit."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6462:
msgid: "Try '{command} {option}' for help."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6467:
msgid: "Invalid value: {message}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6472:
msgid: "Invalid value for {param_hint}: {message}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6476:
msgid: "Missing argument"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_20.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 20 ===
Translations 476-500 of 843
============================================================
Line 6486:
msgid: "Missing parameter"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6491:
msgid: "Missing {param_type}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6496:
msgid: "Missing parameter: {param_name}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6501:
msgid: "No such option: {name}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6516:
msgid: "unknown error"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6520:
msgid: "Could not open file {filename!r}: {message}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6530:
msgid: "Argument {name!r} takes {nargs} values."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6534:
msgid: "Option {name!r} does not take a value."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6548:
msgid: "Shell completion is not supported for Bash versions older than 4.4."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6552:
msgid: "Couldn't detect Bash version, shell completion is not supported."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6562:
msgid: "Error: The value you entered was invalid."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6572:
msgid: "Error: The two entered values do not match."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6576:
msgid: "Error: invalid input"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6580:
msgid: "Press any key to continue..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6585:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6611:
msgid: "{value!r} is not a valid {number_type}."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6616:
msgid: "{value} is not in the range {range}."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6620:
msgid: "{value!r} is not a valid boolean. Recognized values: {states}"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6624:
msgid: "{value!r} is not a valid UUID."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6634:
msgid: "directory"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6638:
msgid: "path"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6642:
msgid: "{name} {filename!r} does not exist."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6646:
msgid: "{name} {filename!r} is a file."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6650:
msgid: "{name} {filename!r} is a directory."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6654:
msgid: "{name} {filename!r} is not readable."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_21.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 21 ===
Translations 501-525 of 843
============================================================
Line 6658:
msgid: "{name} {filename!r} is not writable."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6662:
msgid: "{name} {filename!r} is not executable."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6677:
msgid: "RoW"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6682:
msgid: "GLO"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6686:
msgid: "RoE"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6696:
msgid: "Site Maps"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6700:
msgid: "Static Files"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6712:
msgid: "…"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6716:
msgid: "That page number is not an integer"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6720:
msgid: "That page number is less than 1"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6724:
msgid: "That page contains no results"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6728:
msgid: "Enter a valid value."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6739:
msgid: "Enter a valid URL."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6743:
msgid: "Enter a valid integer."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6747:
msgid: "Enter a valid email address."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6752:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6757:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6767:
msgid: "Enter a valid %(protocol)s address."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6771:
msgid: "IPv4"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6776:
msgid: "IPv6"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6780:
msgid: "IPv4 or IPv6"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6784:
msgid: "Enter only digits separated by commas."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6789:
msgid: "Ensure this value is %(limit_value)s (it is %(show_value)s)."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6794:
msgid: "Ensure this value is less than or equal to %(limit_value)s."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6799:
msgid: "Ensure this value is greater than or equal to %(limit_value)s."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_22.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 22 ===
Translations 526-550 of 843
============================================================
Line 6804:
msgid: "Ensure this value is a multiple of step size %(limit_value)s."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6809:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6889:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6895:
msgid: "Null characters are not allowed."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6901:
msgid: "and"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6906:
msgid: "%(model_name)s with this %(field_labels)s already exists."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6911:
msgid: "Constraint “%(name)s” is violated."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6916:
msgid: "Value %(value)r is not a valid choice."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6920:
msgid: "This field cannot be null."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6924:
msgid: "This field cannot be blank."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6929:
msgid: "%(model_name)s with this %(field_label)s already exists."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6936:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6942:
msgid: "Field of type: %(field_type)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6947:
msgid: "“%(value)s” value must be either True or False."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6952:
msgid: "“%(value)s” value must be either True, False, or None."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6956:
msgid: "Boolean (Either True or False)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6961:
msgid: "String (up to %(max_length)s)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6965:
msgid: "String (unlimited)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6976:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6984:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6990:
msgid: "Date (without time)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 6995:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7002:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7008:
msgid: "Date (with time)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7013:
msgid: "“%(value)s” value must be a decimal number."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_23.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 23 ===
Translations 551-575 of 843
============================================================
Line 7017:
msgid: "Decimal number"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7022:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7041:
msgid: "“%(value)s” value must be a float."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7052:
msgid: "“%(value)s” value must be an integer."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7062:
msgid: "Big (8 byte) integer"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7066:
msgid: "Small integer"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7084:
msgid: "“%(value)s” value must be either None, True or False."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7088:
msgid: "Boolean (Either True, False or None)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7092:
msgid: "Positive big integer"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7096:
msgid: "Positive integer"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7100:
msgid: "Positive small integer"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7105:
msgid: "Slug (up to %(max_length)s)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7109:
msgid: "Text"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7114:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7121:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7137:
msgid: "URL"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7141:
msgid: "Raw binary data"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7146:
msgid: "“%(value)s” is not a valid UUID."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7150:
msgid: "Universally unique identifier"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7154:
msgid: "Image"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7158:
msgid: "A JSON object"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7162:
msgid: "Value must be valid JSON."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7167:
msgid: "%(model)s instance with %(field)s %(value)r is not a valid choice."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7171:
msgid: "Foreign Key (type determined by related field)"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7175:
msgid: "One-to-one relationship"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_24.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 24 ===
Translations 576-600 of 843
============================================================
Line 7180:
msgid: "%(from)s-%(to)s relationship"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7185:
msgid: "%(from)s-%(to)s relationships"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7189:
msgid: "Many-to-many relationship"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7195:
msgid: ":?.!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7199:
msgid: "This field is required."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7223:
msgid: "Enter a valid date/time."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7234:
msgid: "The number of days must be between {min_days} and {max_days}."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7238:
msgid: "No file was submitted. Check the encoding type on the form."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7242:
msgid: "No file was submitted."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7246:
msgid: "The submitted file is empty."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7262:
msgid: "Please either submit a file or check the clear checkbox, not both."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7266:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7275:
msgid: "Select a valid choice. %(value)s is not one of the available choices."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7293:
msgid: "Enter a valid UUID."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7297:
msgid: "Enter a valid JSON."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7302:
msgid: ":"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7307:
msgid: "(Hidden field %(name)s) %(error)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7312:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7341:
msgid: "Order"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7346:
msgid: "Please correct the duplicate data for %(field)s."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7351:
msgid: "Please correct the duplicate data for %(field)s, which must be unique."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7356:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7362:
msgid: "Please correct the duplicate values below."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7366:
msgid: "The inline value did not match the parent instance."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7370:
msgid: "Select a valid choice. That choice is not one of the available choices."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_25.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 25 ===
Translations 601-625 of 843
============================================================
Line 7375:
msgid: "“%(pk)s” is not a valid value."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7380:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7386:
msgid: "Currently"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7400:
msgid: "Unknown"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7405:
msgid: "yes,no,maybe"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7422:
msgid: "%s KB"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7427:
msgid: "%s MB"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7432:
msgid: "%s GB"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7437:
msgid: "%s TB"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7442:
msgid: "%s PB"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7446:
msgid: "p.m."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7450:
msgid: "a.m."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7454:
msgid: "PM"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7458:
msgid: "AM"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7462:
msgid: "midnight"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7466:
msgid: "noon"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7470:
msgid: "Monday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7474:
msgid: "Tuesday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7478:
msgid: "Wednesday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7482:
msgid: "Thursday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7486:
msgid: "Friday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7490:
msgid: "Saturday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7494:
msgid: "Sunday"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7498:
msgid: "Mon"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7502:
msgid: "Tue"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_26.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 26 ===
Translations 626-650 of 843
============================================================
Line 7506:
msgid: "Wed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7510:
msgid: "Thu"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7514:
msgid: "Fri"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7524:
msgid: "Sun"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7528:
msgid: "January"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7532:
msgid: "February"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7542:
msgid: "April"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7546:
msgid: "May"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7550:
msgid: "June"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7554:
msgid: "July"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7558:
msgid: "August"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7562:
msgid: "September"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7566:
msgid: "October"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7570:
msgid: "November"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7574:
msgid: "December"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7578:
msgid: "jan"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7582:
msgid: "feb"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7592:
msgid: "apr"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7602:
msgid: "jun"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7606:
msgid: "jul"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7616:
msgid: "sep"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7620:
msgid: "oct"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7624:
msgid: "nov"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7628:
msgid: "dec"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7633:
msgid: "Jan."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_27.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 27 ===
Translations 651-675 of 843
============================================================
Line 7638:
msgid: "Feb."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7650:
msgid: "April"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7655:
msgid: "May"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7660:
msgid: "June"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7665:
msgid: "July"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7670:
msgid: "Aug."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7675:
msgid: "Sept."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7680:
msgid: "Oct."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7685:
msgid: "Nov."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7690:
msgid: "Dec."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7695:
msgid: "January"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7700:
msgid: "February"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7712:
msgid: "April"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7717:
msgid: "May"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7722:
msgid: "June"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7727:
msgid: "July"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7732:
msgid: "August"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7737:
msgid: "September"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7742:
msgid: "October"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7747:
msgid: "November"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7752:
msgid: "December"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7756:
msgid: "This is not a valid IPv6 address."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7762:
msgid: "%(truncated_text)s…"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7774:
msgid: ", "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7844:
msgid: "Forbidden"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_28.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 28 ===
Translations 676-700 of 843
============================================================
Line 7848:
msgid: "CSRF verification failed. Request aborted."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7860:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7876:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7883:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7889:
msgid: "More information is available with DEBUG=True."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7893:
msgid: "No year specified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7899:
msgid: "Date out of range"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7903:
msgid: "No month specified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7907:
msgid: "No day specified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7911:
msgid: "No week specified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7917:
msgid: "No %(verbose_name_plural)s available"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7922:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7929:
msgid: "Invalid date string “%(datestr)s” given format “%(format)s”"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7934:
msgid: "No %(verbose_name)s found matching the query"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7938:
msgid: "Page is not “last”, nor can it be converted to an int."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7943:
msgid: "Invalid page (%(page_number)s): %(message)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7948:
msgid: "Empty list and “%(class_name)s.allow_empty” is False."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7952:
msgid: "Directory indexes are not allowed here."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7957:
msgid: "“%(path)s” does not exist"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7964:
msgid: "Index of %(directory)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7969:
msgid: "The install worked successfully! Congratulations!"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7974:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7989:
msgid: "Django Documentation"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7993:
msgid: "Topics, references, &amp; how-tos"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 7997:
msgid: "Tutorial: A Polling App"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_29.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 29 ===
Translations 701-725 of 843
============================================================
Line 8001:
msgid: "Get started with Django"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8005:
msgid: "Django Community"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8009:
msgid: "Connect, get help, or contribute"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8013:
msgid: "You do not have permission to upload files."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8017:
msgid: "You must be logged in to upload files."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8022:
msgid: "File should be at most %(max_size)s MB."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8026:
msgid: "Invalid form data"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8030:
msgid: "Check the correct settings.CKEDITOR_5_CONFIGS "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8034:
msgid: "Only POST method is allowed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8038:
msgid: "Attachment module is disabled"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8042:
msgid: "Only authenticated users are allowed"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8046:
msgid: "No files were requested"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8050:
msgid: "File size exceeds the limit allowed and cannot be saved"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8054:
msgid: "Failed to save attachment"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8059:
msgid: "Attempting to connect to qpid with SASL mechanism %s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8064:
msgid: "Connected to qpid with SASL mechanism %s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8069:
msgid: "Unable to connect to qpid with SASL mechanism %s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8074:
msgid: "required"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8079:
msgid: "Arguments"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8089:
msgid: "[default: {}]"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8093:
msgid: "[env var: {}]"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8097:
msgid: "[required]"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8101:
msgid: "Aborted."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8106:
msgid: "Try [blue]'{command_path} {help_option}'[/] for help."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8123:
msgid: "Collapse"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_30.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 30 ===
Translations 726-750 of 843
============================================================
Line 8128:
msgid: "Value"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8132:
msgid: "Default"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8137:
msgid: "Code"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8141:
msgid: "Modified"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8145:
msgid: "Reset to default"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8166:
msgid: " By %(filter_title)s "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8171:
msgid: "To"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8175:
msgid: "Date from"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8179:
msgid: "Date to"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8195:
msgid: "Paragraph"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8199:
msgid: "Underlined"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8203:
msgid: "Bold"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8207:
msgid: "Italic"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8211:
msgid: "Strike"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8218:
msgid: "Heading"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8222:
msgid: "Quote"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8226:
msgid: "Unordered list"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8230:
msgid: "Ordered list"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8234:
msgid: "Indent increase"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8238:
msgid: "Indent decrease"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8242:
msgid: "Undo"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8246:
msgid: "Redo"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8256:
msgid: "Unlink"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8260:
msgid: "Object permissions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8267:
msgid: "Object"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_31.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 31 ===
Translations 751-775 of 843
============================================================
Line 8272:
msgid: "Group"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8276:
msgid: "Group permissions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8286:
msgid: "User permissions"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8297:
msgid: "Export"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8301:
msgid: "Import"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8322:
msgid: "This exporter will export the following fields"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8326:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8344:
msgid: "Skipped"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8348:
msgid: "Some rows failed to validate"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8352:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8359:
msgid: "Row"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8370:
msgid: "Non field specific"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8374:
msgid: "This exporter will export the following fields: "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8378:
msgid: "This importer will import the following fields: "
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8385:
msgid: "%(class_name)s %(instance)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8390:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8396:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8415:
msgid: "This object doesn't have a change history."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8419:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8424:
msgid: "Press the 'Change History' button below to edit the history."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8431:
msgid: "Date/time"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8449:
msgid: "None"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8453:
msgid: "Revert"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8469:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8483:
msgid: "Run the selected action"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_32.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 32 ===
Translations 776-800 of 843
============================================================
Line 8487:
msgid: "Run"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8491:
msgid: "Click here to select the objects across all pages"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8496:
msgid: "Select all %(total_count)s %(module_name)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8500:
msgid: "Clear selection"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8506:
msgid: "Models in the %(name)s application"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8512:
msgid: "You dont have permission to view or edit anything."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8516:
msgid: "After you've created a user, youll be able to edit more user options."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8521:
msgid: "Enter a new password for the user <strong>%(username)s</strong>."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8533:
msgid: "History"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8545:
msgid: "Filters"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8549:
msgid: "Select all rows"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8559:
msgid: "Remove from sorting"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8564:
msgid: "Sorting priority: %(priority_number)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8569:
msgid: "Expand row"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8574:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8582:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8589:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8596:
msgid: "Objects"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8601:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8609:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8616:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8626:
msgid: "Welcome back to"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8631:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8637:
msgid: "Log in"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8641:
msgid: "Forgotten your password or username?"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

204
translation_batch_33.txt Normal file
View File

@ -0,0 +1,204 @@
=== TRANSLATION BATCH 33 ===
Translations 801-825 of 843
============================================================
Line 8645:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8653:
msgid: "Show all"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8658:
msgid: "Type to search"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8662:
msgid: "Save and continue editing"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8666:
msgid: "Save and view"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8670:
msgid: "Save and add another"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8674:
msgid: "Save as new"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8678:
msgid: "You have been successfully logged out from the administration"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8682:
msgid: "Thanks for spending some quality time with the web site today."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8686:
msgid: "Log in again"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8690:
msgid: "Your password was changed."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8694:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8721:
msgid: "Add %(name)s"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8731:
msgid: "Add"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8735:
msgid: "True"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8739:
msgid: "False"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8743:
msgid: "Hide counts"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8753:
msgid: "Clear all filters"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8757:
msgid: "Recent searches"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8761:
msgid: "No recent searches"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8771:
msgid: "Loading more results..."
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8792:
msgid: "No, take me back"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8796:
msgid: "Yes, Im sure"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8800:
msgid: "Record picture"
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------
Line 8816:
msgid: ""
msgstr: ""
Arabic Translation:
msgstr: ""
----------------------------------------

Some files were not shown because too many files have changed in this diff Show More