Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend

This commit is contained in:
Faheed 2025-11-18 13:53:03 +03:00
commit b4b56d8a9d
29 changed files with 405 additions and 190 deletions

6
.env
View File

@ -1,3 +1,3 @@
DB_NAME=haikal_db DB_NAME=norahuniversity
DB_USER=faheed DB_USER=norahuniversity
DB_PASSWORD=Faheed@215 DB_PASSWORD=norahuniversity

View File

@ -487,3 +487,6 @@ MESSAGE_TAGS = {
# Custom User Model # Custom User Model
AUTH_USER_MODEL = "recruitment.CustomUser" AUTH_USER_MODEL = "recruitment.CustomUser"
ZOOM_WEBHOOK_API_KEY = "2GNDC5Rvyw9AHoGikHXsQB"

View File

@ -55,7 +55,7 @@ class SourceForm(forms.ModelForm):
class Meta: class Meta:
model = Source model = Source
fields = ["name", "source_type", "description", "ip_address", "is_active"] fields = ["name", "source_type", "description", "ip_address","trusted_ips", "is_active"]
widgets = { widgets = {
"name": forms.TextInput( "name": forms.TextInput(
attrs={ attrs={
@ -81,6 +81,9 @@ class SourceForm(forms.ModelForm):
"ip_address": forms.TextInput( "ip_address": forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100"} attrs={"class": "form-control", "placeholder": "192.168.1.100"}
), ),
"trusted_ips":forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100","required": False}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}), "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
} }
@ -2228,7 +2231,7 @@ class CandidateSignupForm(forms.ModelForm):
'last_name': forms.TextInput(attrs={'class': 'form-control'}), 'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}), 'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}), 'phone': forms.TextInput(attrs={'class': 'form-control'}),
'gpa': forms.TextInput(attrs={'class': 'form-control'}), # 'gpa': forms.TextInput(attrs={'class': 'form-control'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}), "nationality": forms.Select(attrs={'class': 'form-control select2'}),
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'gender': forms.Select(attrs={'class': 'form-control'}), 'gender': forms.Select(attrs={'class': 'form-control'}),

View File

@ -0,0 +1,29 @@
# Generated by Django 5.2.6 on 2025-11-18 10:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='job_type',
field=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),
),
migrations.AlterField(
model_name='jobposting',
name='workplace_type',
field=models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20),
),
migrations.AlterField(
model_name='scheduledinterview',
name='interview_location',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interviewlocation', verbose_name='Meeting/Location Details'),
),
]

View File

@ -99,9 +99,9 @@ class JobPosting(Base):
# Core Fields # Core Fields
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
department = models.CharField(max_length=100, blank=True) department = models.CharField(max_length=100, blank=True)
job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="FULL_TIME") job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="Full-time")
workplace_type = models.CharField( workplace_type = models.CharField(
max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE" max_length=20, choices=WORKPLACE_TYPES, default="On-site"
) )
# Location # Location

View File

@ -18,7 +18,9 @@ from .models import (
Notification, Notification,
HiringAgency, HiringAgency,
Person, Person,
Source,
) )
from .forms import generate_api_key, generate_api_secret
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,11 +31,10 @@ User = get_user_model()
@receiver(post_save, sender=JobPosting) @receiver(post_save, sender=JobPosting)
def format_job(sender, instance, created, **kwargs): def format_job(sender, instance, created, **kwargs):
if created or not instance.ai_parsed: if created or not instance.ai_parsed:
try: form = getattr(instance, "form_template", None)
form_template = instance.form_template if not form:
except FormTemplate.DoesNotExist:
FormTemplate.objects.get_or_create( FormTemplate.objects.get_or_create(
job=instance, is_active=False, name=instance.title job=instance, is_active=True, name=instance.title
) )
async_task( async_task(
"recruitment.tasks.format_job_description", "recruitment.tasks.format_job_description",
@ -469,3 +470,27 @@ def person_created(sender, instance, created, **kwargs):
) )
instance.user = user instance.user = user
instance.save() instance.save()
@receiver(post_save, sender=Source)
def source_created(sender, instance, created, **kwargs):
"""
Automatically generate API key and API secret when a new Source is created.
"""
if created:
# Only generate keys if they don't already exist
if not instance.api_key and not instance.api_secret:
logger.info(f"Generating API keys for new Source: {instance.pk} - {instance.name}")
# Generate API key and secret using existing secure functions
api_key = generate_api_key()
api_secret = generate_api_secret()
# Update the source with generated keys
instance.api_key = api_key
instance.api_secret = api_secret
instance.save(update_fields=['api_key', 'api_secret'])
logger.info(f"API keys generated successfully for Source: {instance.name} (Key: {api_key[:8]}...)")
else:
logger.info(f"Source {instance.name} already has API keys, skipping generation")

View File

@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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' OPENROUTER_API_KEY ='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'
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' # OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
OPENROUTER_MODEL = 'openai/gpt-oss-20b' OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct'
# OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'openai/gpt-oss-20b'
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free' # OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
@ -506,6 +506,7 @@ def handle_zoom_webhook_event(payload):
Background task to process a Zoom webhook event and update the local ZoomMeeting status. Background task to process a Zoom webhook event and update the local ZoomMeeting status.
It handles: created, updated, started, ended, and deleted events. It handles: created, updated, started, ended, and deleted events.
""" """
print(payload)
event_type = payload.get('event') event_type = payload.get('event')
object_data = payload['payload']['object'] object_data = payload['payload']['object']
@ -534,7 +535,9 @@ def handle_zoom_webhook_event(payload):
# elif event_type == 'meeting.updated': # elif event_type == 'meeting.updated':
# Only update time fields if they are in the payload # Only update time fields if they are in the payload
print(object_data) print(object_data)
meeting_instance.start_time = object_data.get('start_time', meeting_instance.start_time) meeting_start_time = object_data.get('start_time', meeting_instance.start_time)
if meeting_start_time:
meeting_instance.start_time = datetime.fromisoformat(meeting_start_time)
meeting_instance.duration = object_data.get('duration', meeting_instance.duration) meeting_instance.duration = object_data.get('duration', meeting_instance.duration)
meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone) meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone)

View File

@ -1336,6 +1336,7 @@ def application_submit(request, template_slug):
# email = submission.responses.get(field__label="Email Address") # email = submission.responses.get(field__label="Email Address")
# phone = submission.responses.get(field__label="Phone Number") # phone = submission.responses.get(field__label="Phone Number")
# address = submission.responses.get(field__label="Address") # address = submission.responses.get(field__label="Address")
gpa = submission.responses.get(field__label="GPA")
resume = submission.responses.get(field__label="Resume Upload") resume = submission.responses.get(field__label="Resume Upload")
@ -1346,6 +1347,8 @@ def application_submit(request, template_slug):
submission.save() submission.save()
# time=timezone.now() # time=timezone.now()
person = request.user.person_profile person = request.user.person_profile
person.gpa = gpa.value if gpa else None
person.save()
Application.objects.create( Application.objects.create(
person = person, person = person,
resume=resume.get_file if resume.is_file else None, resume=resume.get_file if resume.is_file else None,
@ -1806,7 +1809,7 @@ def candidate_screening_view(request, slug):
min_experience_str = request.GET.get("min_experience") min_experience_str = request.GET.get("min_experience")
screening_rating = request.GET.get("screening_rating") screening_rating = request.GET.get("screening_rating")
tier1_count_str = request.GET.get("tier1_count") tier1_count_str = request.GET.get("tier1_count")
gpa = request.GET.get("gpa") gpa = request.GET.get("GPA")
try: try:
# Check if the string value exists and is not an empty string before conversion # Check if the string value exists and is not an empty string before conversion
@ -1854,8 +1857,9 @@ def candidate_screening_view(request, slug):
) )
if gpa: if gpa:
candidates = candidates.filter( candidates = candidates.filter(
person__gpa = gpa person__gpa__gt= gpa
) )
print(candidates)
if tier1_count > 0: if tier1_count > 0:
candidates = candidates[:tier1_count] candidates = candidates[:tier1_count]
@ -3018,7 +3022,6 @@ def is_superuser_check(user):
def create_staff_user(request): def create_staff_user(request):
if request.method == "POST": if request.method == "POST":
form = StaffUserCreationForm(request.POST) form = StaffUserCreationForm(request.POST)
print(form)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success( messages.success(
@ -3034,7 +3037,7 @@ def create_staff_user(request):
@staff_user_required @staff_user_required
def admin_settings(request): def admin_settings(request):
staffs = User.objects.filter(is_superuser=False) staffs = User.objects.filter(user_type="staff",is_superuser=False)
form = ToggleAccountForm() form = ToggleAccountForm()
context = {"staffs": staffs, "form": form} context = {"staffs": staffs, "form": form}
return render(request, "user/admin_settings.html", context) return render(request, "user/admin_settings.html", context)
@ -3097,12 +3100,11 @@ def account_toggle_status(request, pk):
@csrf_exempt @csrf_exempt
@staff_user_required
def zoom_webhook_view(request): def zoom_webhook_view(request):
print(request.headers) api_key = request.headers.get("X-Zoom-API-KEY")
print(settings.ZOOM_WEBHOOK_API_KEY) if api_key != settings.ZOOM_WEBHOOK_API_KEY:
# if api_key != settings.ZOOM_WEBHOOK_API_KEY: return HttpResponse(status=405)
# return HttpResponse(status=405)
if request.method == "POST": if request.method == "POST":
try: try:
payload = json.loads(request.body) payload = json.loads(request.body)
@ -5497,7 +5499,7 @@ def candidate_signup(request, slug):
gender = form.cleaned_data["gender"] gender = form.cleaned_data["gender"]
nationality = form.cleaned_data["nationality"] nationality = form.cleaned_data["nationality"]
address = form.cleaned_data["address"] address = form.cleaned_data["address"]
gpa = form.cleaned_data["gpa"] # gpa = form.cleaned_data["gpa"]
password = form.cleaned_data["password"] password = form.cleaned_data["password"]
user = User.objects.create_user( user = User.objects.create_user(
@ -5512,7 +5514,7 @@ def candidate_signup(request, slug):
phone=phone, phone=phone,
gender=gender, gender=gender,
nationality=nationality, nationality=nationality,
gpa=gpa, # gpa=gpa,
address=address, address=address,
user = user user = user
) )

View File

@ -204,26 +204,27 @@ def generate_api_keys_view(request, pk):
source.save() source.save()
# Log the key regeneration # Log the key regeneration
IntegrationLog.objects.create( # IntegrationLog.objects.create(
source=source, # source=source,
action=IntegrationLog.ActionChoices.CREATE, # action=IntegrationLog.ActionChoices.CREATE,
endpoint=f'/api/sources/{source.pk}/generate-keys/', # endpoint=f'/api/sources/{source.pk}/generate-keys/',
method='POST', # method='POST',
request_data={ # request_data={
'name': source.name, # 'name': source.name,
'old_api_key': old_api_key[:8] + '...' if old_api_key else None, # 'old_api_key': old_api_key[:8] + '...' if old_api_key else None,
'new_api_key': new_api_key[:8] + '...' # 'new_api_key': new_api_key[:8] + '...'
}, # },
ip_address=request.META.get('REMOTE_ADDR'), # ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '') # user_agent=request.META.get('HTTP_USER_AGENT', '')
) # )
return JsonResponse({ return redirect('source_detail', pk=source.pk)
'success': True, # return JsonResponse({
'api_key': new_api_key, # 'success': True,
'api_secret': new_api_secret, # 'api_key': new_api_key,
'message': 'API keys regenerated successfully' # 'api_secret': new_api_secret,
}) # 'message': 'API keys regenerated successfully'
# })
return JsonResponse({'error': 'Invalid request method'}, status=405) return JsonResponse({'error': 'Invalid request method'}, status=405)
@ -244,27 +245,28 @@ def toggle_source_status_view(request, pk):
source.save() source.save()
# Log the status change # Log the status change
IntegrationLog.objects.create( # IntegrationLog.objects.create(
source=source, # source=source,
action=IntegrationLog.ActionChoices.SYNC, # action=IntegrationLog.ActionChoices.SYNC,
endpoint=f'/api/sources/{source.pk}/toggle-status/', # endpoint=f'/api/sources/{source.pk}/toggle-status/',
method='POST', # method='POST',
request_data={ # request_data={
'name': source.name, # 'name': source.name,
'old_status': old_status, # 'old_status': old_status,
'new_status': source.is_active # 'new_status': source.is_active
}, # },
ip_address=request.META.get('REMOTE_ADDR'), # ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '') # user_agent=request.META.get('HTTP_USER_AGENT', '')
) # )
status_text = 'activated' if source.is_active else 'deactivated' status_text = 'activated' if source.is_active else 'deactivated'
return JsonResponse({ return redirect('source_detail', pk=source.pk)
'success': True, # return JsonResponse({
'is_active': source.is_active, # 'success': True,
'message': f'Source "{source.name}" {status_text} successfully' # 'is_active': source.is_active,
}) # 'message': f'Source "{source.name}" {status_text} successfully'
# })
def copy_to_clipboard_view(request): def copy_to_clipboard_view(request):
"""HTMX endpoint to copy text to clipboard""" """HTMX endpoint to copy text to clipboard"""

View File

@ -32,8 +32,8 @@
--gray-text: #6c757d; --gray-text: #6c757d;
--kaauh-border: #d0d7de; /* Cleaner border color */ --kaauh-border: #d0d7de; /* Cleaner border color */
--kaauh-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); /* Deeper shadow for premium look */ --kaauh-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); /* Deeper shadow for premium look */
--kaauh-dark-bg: #0d0d0d; --kaauh-dark-bg: #0d0d0d;
--kaauh-dark-contrast: #1c1c1c; --kaauh-dark-contrast: #1c1c1c;
/* CALCULATED STICKY HEIGHTS (As provided in base) */ /* CALCULATED STICKY HEIGHTS (As provided in base) */
--navbar-height: 56px; --navbar-height: 56px;
@ -43,17 +43,145 @@
body { body {
min-height: 100vh; min-height: 100vh;
background-color: #f0f0f5; background-color: #f0f0f5;
padding-top: 0; padding-top: 0;
} }
.text-primary-theme { color: var(--kaauh-teal) !important; } .text-primary-theme { color: var(--kaauh-teal) !important; }
.text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; } .text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; }
/* Language Dropdown Styles */
.language-toggle-btn {
background-color: transparent;
border: 1px solid var(--kaauh-border);
color: var(--kaauh-teal);
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
min-width: 120px;
justify-content: center;
}
.language-toggle-btn:hover {
background-color: var(--kaauh-teal-light);
border-color: var(--kaauh-teal);
color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 99, 110, 0.15);
}
.language-toggle-btn:focus {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
border-color: var(--kaauh-teal);
}
.language-toggle-btn::after {
margin-left: 0.5rem;
}
.dropdown-menu {
border: 1px solid var(--kaauh-border);
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
padding: 0.5rem;
min-width: 180px;
}
.dropdown-menu .dropdown-item {
padding: 0.75rem 1rem;
transition: all 0.2s ease;
border-radius: 0.375rem;
margin: 0.25rem 0;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 500;
border: none;
background: transparent;
width: 100%;
text-align: left;
}
.dropdown-menu .dropdown-item:hover {
background-color: var(--kaauh-teal);
color: white;
transform: translateX(4px);
}
.dropdown-menu .dropdown-item:focus {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.dropdown-menu .dropdown-item.active {
background-color: var(--kaauh-teal);
color: white;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 99, 110, 0.2);
}
.dropdown-menu .dropdown-item.active:hover {
background-color: var(--kaauh-teal-dark);
}
.flag-emoji {
font-size: 1.2rem;
line-height: 1;
min-width: 24px;
text-align: center;
}
.language-text {
font-size: 0.9rem;
font-weight: 500;
}
/* RTL Support for Language Dropdown */
html[dir="rtl"] .language-toggle-btn {
flex-direction: row-reverse;
}
html[dir="rtl"] .dropdown-menu .dropdown-item {
flex-direction: row-reverse;
text-align: right;
}
html[dir="rtl"] .dropdown-menu .dropdown-item:hover {
transform: translateX(-4px);
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.language-toggle-btn {
min-width: 100px;
padding: 0.4rem 0.8rem;
font-size: 0.9rem;
}
.dropdown-menu {
min-width: 160px;
}
.dropdown-menu .dropdown-item {
padding: 0.6rem 0.8rem;
font-size: 0.85rem;
}
.flag-emoji {
font-size: 1rem;
min-width: 20px;
}
}
.bg-kaauh-teal { .bg-kaauh-teal {
background-color: #00636e; background-color: #00636e;
} }
.btn-main-action { .btn-main-action {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal);
color: white; color: white;
@ -67,7 +195,7 @@
background-color: var(--kaauh-teal-dark); background-color: var(--kaauh-teal-dark);
color: white; color: white;
transform: translateY(-2px); /* More pronounced lift */ transform: translateY(-2px); /* More pronounced lift */
box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5); box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5);
} }
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
/* 1. DARK HERO STYLING (High Contrast) */ /* 1. DARK HERO STYLING (High Contrast) */
@ -75,16 +203,16 @@
.hero-section { .hero-section {
background: linear-gradient(135deg, var(--kaauh-dark-contrast) 0%, var(--kaauh-dark-bg) 100%); background: linear-gradient(135deg, var(--kaauh-dark-contrast) 0%, var(--kaauh-dark-bg) 100%);
padding: 4rem 0; /* Reduced from 8rem to 4rem */ padding: 4rem 0; /* Reduced from 8rem to 4rem */
margin-top: -1px; margin-top: -1px;
color: white; color: white;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.hero-title { .hero-title {
font-size: 2.5rem; /* Reduced from 3.5rem to 2.5rem */ font-size: 2.5rem; /* Reduced from 3.5rem to 2.5rem */
font-weight: 800; /* Extra bold */ font-weight: 800; /* Extra bold */
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.05em; letter-spacing: -0.05em;
max-width: 900px; max-width: 900px;
} }
.hero-section .lead { .hero-section .lead {
@ -104,7 +232,7 @@
padding: 10rem 0; padding: 10rem 0;
} }
.hero-title { .hero-title {
font-size: 5.5rem; font-size: 5.5rem;
} }
} }
@ -153,20 +281,20 @@
background-color: #f0f0f5; /* Separates the job list from the white path section */ background-color: #f0f0f5; /* Separates the job list from the white path section */
padding-top: 3rem; padding-top: 3rem;
} }
.job-listing-card { .job-listing-card {
border: 1px solid var(--kaauh-border); border: 1px solid var(--kaauh-border);
border-left: 6px solid var(--kaauh-teal); border-left: 6px solid var(--kaauh-teal);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 2rem !important; padding: 2rem !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); /* Lighter default shadow */ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); /* Lighter default shadow */
} }
.job-listing-card:hover { .job-listing-card:hover {
transform: translateY(-3px); /* Increased lift */ transform: translateY(-3px); /* Increased lift */
box-shadow: 0 12px 25px rgba(0, 99, 110, 0.15); /* Stronger hover shadow */ box-shadow: 0 12px 25px rgba(0, 99, 110, 0.15); /* Stronger hover shadow */
background-color: var(--kaauh-teal-light); background-color: var(--kaauh-teal-light);
} }
.card.sticky-top-filters { .card.sticky-top-filters {
box-shadow: var(--kaauh-shadow); /* Uses the deeper card shadow */ box-shadow: var(--kaauh-shadow); /* Uses the deeper card shadow */
} }
@ -215,7 +343,8 @@
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %} <form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}"> <input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit"> <button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇺🇸</span> English <span class="flag-emoji">🇺🇸</span>
<span class="language-text">English</span>
</button> </button>
</form> </form>
</li> </li>
@ -224,7 +353,8 @@
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %} <form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}"> <input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit"> <button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇸🇦</span> العربية (Arabic) <span class="flag-emoji">🇸🇦</span>
<span class="language-text">العربية (Arabic)</span>
</button> </button>
</form> </form>
</li> </li>
@ -239,7 +369,7 @@
<div class="container message-container mt-3"> <div class="container message-container mt-3">
<div class="row"> <div class="row">
{# Use responsive columns matching the main content block for alignment #} {# Use responsive columns matching the main content block for alignment #}
<div class="col-lg-12 order-lg-1 col-12 mx-auto"> <div class="col-lg-12 order-lg-1 col-12 mx-auto">
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i> {{ message }} <i class="fas fa-check-circle me-2"></i> {{ message }}
@ -254,20 +384,20 @@
{# ================================================= #} {# ================================================= #}
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #} {# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
{# ================================================= #} {# ================================================= #}
{# ================================================= #} {# ================================================= #}
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
<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/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block customJS %} {% block customJS %}
{% endblock %} {% endblock %}
</body> </body>
</html> </html>

View File

@ -250,7 +250,7 @@
<a class="nav-link {% if request.resolver_match.url_name == 'person_list' %}active{% endif %}" href="{% url 'person_list' %}"> <a class="nav-link {% if request.resolver_match.url_name == 'person_list' %}active{% endif %}" href="{% url 'person_list' %}">
<span class="d-flex align-items-center gap-2"> <span class="d-flex align-items-center gap-2">
{% include "icons/users.html" %} {% include "icons/users.html" %}
{% trans "Person" %} {% trans "Applicant" %}
</span> </span>
</a> </a>
</li> </li>

View File

@ -272,7 +272,7 @@
<th scope="col" rowspan="2">{% trans "Actions" %}</th> <th scope="col" rowspan="2">{% trans "Actions" %}</th>
<th scope="col" rowspan="2" class="text-center">{% trans "Manage Forms" %}</th> <th scope="col" rowspan="2" class="text-center">{% trans "Manage Forms" %}</th>
<th scope="col" colspan="5" class="candidate-management-header-title"> <th scope="col" colspan="6" class="candidate-management-header-title">
{% trans "Applicants Metrics" %} {% trans "Applicants Metrics" %}
</th> </th>
</tr> </tr>
@ -282,10 +282,11 @@
<th style="width: calc(50% / 7);">{% trans "Screened" %}</th> <th style="width: calc(50% / 7);">{% trans "Screened" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Exam" %}</th> <th style="width: calc(50% / 7 * 2);">{% trans "Exam" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Interview" %}</th> <th style="width: calc(50% / 7 * 2);">{% trans "Interview" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Documets Review" %}</th>
<th style="width: calc(50% / 7);">{% trans "Offer" %}</th> <th style="width: calc(50% / 7);">{% trans "Offer" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for job in jobs %} {% for job in jobs %}
<tr> <tr>
@ -311,7 +312,7 @@
<td class="text-center"> <td class="text-center">
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
{% if job.form_template %} {% if job.form_template %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}"> <a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary {% if job.status != 'ACTIVE' %}disabled{% endif %}" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> <a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
@ -329,6 +330,7 @@
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td> <td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %}</a></td> <td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td> <td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_document_review_view' job.slug %}" class="text-success">{% if job.document_review_candidates.count %}{{ job.document_review_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_offer_view' job.slug %}" class="text-success">{% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %}</a></td> <td class="candidate-data-cell text-success"><a href="{% url 'candidate_offer_view' job.slug %}" class="text-success">{% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -113,7 +113,7 @@
</a> </a>
{% comment %} CONNECTOR 1 -> 2 {% endcomment %} {% comment %} CONNECTOR 1 -> 2 {% endcomment %}
<div class="stage-connector {% if current_stage == 'Exam' or current_stage == 'Interview' or current_stage == 'Offer' %}completed{% endif %}"></div> <div class="stage-connector {% if current_stage == 'Exam' or current_stage == 'Interview' or current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
{% comment %} STAGE 2: Exam {% endcomment %} {% comment %} STAGE 2: Exam {% endcomment %}
<a href="{% url 'candidate_exam_view' job.slug %}" <a href="{% url 'candidate_exam_view' job.slug %}"
@ -127,7 +127,7 @@
</a> </a>
{% comment %} CONNECTOR 2 -> 3 {% endcomment %} {% comment %} CONNECTOR 2 -> 3 {% endcomment %}
<div class="stage-connector {% if current_stage == 'Interview' or current_stage == 'Offer' %}completed{% endif %}"></div> <div class="stage-connector {% if current_stage == 'Interview' or current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
{% comment %} STAGE 3: Interview {% endcomment %} {% comment %} STAGE 3: Interview {% endcomment %}
<a href="{% url 'candidate_interview_view' job.slug %}" <a href="{% url 'candidate_interview_view' job.slug %}"

View File

@ -152,10 +152,10 @@
<!-- Header --> <!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-friends me-2"></i> {% trans "People Directory" %} <i class="fas fa-user-friends me-2"></i> {% trans "Applicants List" %}
</h1> </h1>
<a href="{% url 'person_create' %}" class="btn btn-main-action"> <a href="{% url 'person_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New Person" %} <i class="fas fa-plus me-1"></i> {% trans "Add New" %}
</a> </a>
</div> </div>
@ -168,7 +168,7 @@
<form method="get" action="" class="w-100"> <form method="get" action="" class="w-100">
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<input type="text" name="q" class="form-control" id="search" <input type="text" name="q" class="form-control" id="search"
placeholder="{% trans 'Search people...' %}" placeholder="{% trans 'Search applicant...' %}"
value="{{ request.GET.q }}"> value="{{ request.GET.q }}">
<button class="btn btn-main-action" type="submit"> <button class="btn btn-main-action" type="submit">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>

View File

@ -89,6 +89,7 @@
.status-applied { background: #e3f2fd; color: #1976d2; } .status-applied { background: #e3f2fd; color: #1976d2; }
.status-screening { background: #fff3e0; color: #f57c00; } .status-screening { background: #fff3e0; color: #f57c00; }
.status-document_review { background: #f3e5f5; color: #7b1fa2; }
.status-exam { background: #f3e5f5; color: #7b1fa2; } .status-exam { background: #f3e5f5; color: #7b1fa2; }
.status-interview { background: #e8f5e8; color: #388e3c; } .status-interview { background: #e8f5e8; color: #388e3c; }
.status-offer { background: #fff8e1; color: #f9a825; } .status-offer { background: #fff8e1; color: #f9a825; }
@ -119,6 +120,9 @@
background-color: #f3e5f5; background-color: #f3e5f5;
border-color: #ce93d8; border-color: #ce93d8;
} }
.card-header{
padding: 0.75rem 1.25rem;
}
</style> </style>
{% endblock %} {% endblock %}
@ -155,7 +159,7 @@
</p> </p>
</div> </div>
<div class="col-md-4 text-end"> <div class="col-md-4 text-end">
<span class="status-badge status-{{ application.stage|lower }}"> <span class="status-badge status-{{ application.stage }}">
{{ application.get_stage_display }} {{ application.get_stage_display }}
</span> </span>
</div> </div>
@ -172,19 +176,9 @@
<div class="progress-label">{% trans "Applied" %}</div> <div class="progress-label">{% trans "Applied" %}</div>
</div> </div>
<!-- Screening Stage - Show if current stage is Screening or beyond -->
{% if application.stage in 'Screening,Exam,Interview,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening' %}completed{% elif application.stage == 'Screening' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-search"></i>
</div>
<div class="progress-label">{% trans "Screening" %}</div>
</div>
{% endif %}
<!-- Exam Stage - Show if current stage is Exam or beyond --> <!-- Exam Stage - Show if current stage is Exam or beyond -->
{% if application.stage in 'Exam,Interview,Offer,Hired,Rejected' %} {% if application.stage in 'Exam,Interview,Document Review,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}"> <div class="progress-step {% if application.stage not in 'Applied,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}">
<div class="progress-icon"> <div class="progress-icon">
<i class="fas fa-clipboard-check"></i> <i class="fas fa-clipboard-check"></i>
</div> </div>
@ -193,8 +187,8 @@
{% endif %} {% endif %}
<!-- Interview Stage - Show if current stage is Interview or beyond --> <!-- Interview Stage - Show if current stage is Interview or beyond -->
{% if application.stage in 'Interview,Offer,Hired,Rejected' %} {% if application.stage in 'Interview,Document Review,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam,Interview' %}completed{% elif application.stage == 'Interview' %}active{% endif %}"> <div class="progress-step {% if application.stage not in 'Applied,Exam,Interview' %}completed{% elif application.stage == 'Interview' %}active{% endif %}">
<div class="progress-icon"> <div class="progress-icon">
<i class="fas fa-video"></i> <i class="fas fa-video"></i>
</div> </div>
@ -202,9 +196,19 @@
</div> </div>
{% endif %} {% endif %}
<!-- Document Review Stage - Show if current stage is Document Review or beyond -->
{% if application.stage in 'Document Review,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Exam,Interview,Document Review' %}completed{% elif application.stage == 'Document Review' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-file-alt"></i>
</div>
<div class="progress-label">{% trans "Document Review" %}</div>
</div>
{% endif %}
<!-- Offer Stage - Show if current stage is Offer or beyond --> <!-- Offer Stage - Show if current stage is Offer or beyond -->
{% if application.stage in 'Offer,Hired,Rejected' %} {% if application.stage in 'Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam,Interview,Offer' %}completed{% elif application.stage == 'Offer' %}active{% endif %}"> <div class="progress-step {% if application.stage not in 'Applied,Exam,Interview,Document Review,Offer' %}completed{% elif application.stage == 'Offer' %}active{% endif %}">
<div class="progress-icon"> <div class="progress-icon">
<i class="fas fa-handshake"></i> <i class="fas fa-handshake"></i>
</div> </div>
@ -525,6 +529,11 @@
<i class="fas fa-search me-2"></i> <i class="fas fa-search me-2"></i>
{% trans "Your application is currently under screening. We are evaluating your qualifications against the job requirements." %} {% trans "Your application is currently under screening. We are evaluating your qualifications against the job requirements." %}
</div> </div>
{% elif application.stage == 'Document Review' %}
<div class="alert alert-purple">
<i class="fas fa-file-alt me-2"></i>
{% trans "Please upload the required documents for review. Our team will evaluate your submitted materials." %}
</div>
{% elif application.stage == 'Exam' %} {% elif application.stage == 'Exam' %}
<div class="alert alert-purple"> <div class="alert alert-purple">
<i class="fas fa-clipboard-check me-2"></i> <i class="fas fa-clipboard-check me-2"></i>

View File

@ -647,30 +647,30 @@
<div class="card shadow-sm mb-2 p-2"> <div class="card shadow-sm mb-2 p-2">
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5> <h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary"> {% comment %} <a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
<i class="fas fa-edit"></i> {% trans "Edit Details" %} <i class="fas fa-edit"></i> {% trans "Edit Details" %}
</a> </a> {% endcomment %}
<a href="{% url 'candidate_delete' candidate.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')"> {% comment %} <a href="{% url 'candidate_delete' candidate.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
<i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %} <i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %}
</a> </a> {% endcomment %}
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %} <i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
</a> </a>
{% if candidate.resume %} {% if candidate.resume %}
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary"> {% comment %} <a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-eye me-1"></i> <i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %} {% trans "View Actual Resume" %}
</a> </a> {% endcomment %}
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary"> <a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
<i class="fas fa-download me-1"></i> <i class="fas fa-download me-1"></i>
{% trans "Download Resume" %} {% trans "Download Resume" %}
</a> </a>
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info"> {% comment %} <a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
<i class="fas fa-file-alt me-1"></i> <i class="fas fa-file-alt me-1"></i>
{% trans "View Resume AI Overview" %} {% trans "View Resume AI Overview" %}
</a> </a> {% endcomment %}
{% endif %} {% endif %}
</div> </div>

View File

@ -252,7 +252,7 @@
{% trans "GPA" %} {% trans "GPA" %}
</label> </label>
<input type="number" name="GPA" id="gpa" class="form-control form-control-sm" <input type="number" name="GPA" id="gpa" class="form-control form-control-sm"
value="{{ gpa }}" min="0" max="4" step="1" value="{{ gpa }}" min="0" max="4"
placeholder="e.g., 4" style="width: 120px;"> placeholder="e.g., 4" style="width: 120px;">
</div> </div>
<div class="col-auto"> <div class="col-auto">

View File

@ -69,7 +69,7 @@
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-4 mb-3">
<label for="{{ form.first_name.id_for_label }}" class="form-label"> <label for="{{ form.first_name.id_for_label }}" class="form-label">
{% trans "First Name" %} <span class="text-danger">*</span> {% trans "First Name" %} <span class="text-danger">*</span>
</label> </label>
@ -81,7 +81,19 @@
{% endif %} {% endif %}
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-4 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-4 mb-3">
<label for="{{ form.last_name.id_for_label }}" class="form-label"> <label for="{{ form.last_name.id_for_label }}" class="form-label">
{% trans "Last Name" %} <span class="text-danger">*</span> {% trans "Last Name" %} <span class="text-danger">*</span>
</label> </label>
@ -95,19 +107,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-4 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"> <label for="{{ form.phone.id_for_label }}" class="form-label">
{% trans "Phone Number" %} <span class="text-danger">*</span> {% trans "Phone Number" %} <span class="text-danger">*</span>
</label> </label>
@ -118,21 +118,8 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.gpa.id_for_label }}" class="form-label">
{% trans "GPA" %} <span class="text-danger">*</span>
</label>
{{ form.gpa }}
{% if form.nationality.errors %}
<div class="text-danger small">
{{ form.gpa.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3"> <div class="col-md-4 mb-3">
<label for="{{ form.nationality.id_for_label }}" class="form-label"> <label for="{{ form.nationality.id_for_label }}" class="form-label">
{% trans "Nationality" %} <span class="text-danger">*</span> {% trans "Nationality" %} <span class="text-danger">*</span>
</label> </label>
@ -144,7 +131,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-4 mb-3">
<label for="{{ form.gender.id_for_label }}" class="form-label"> <label for="{{ form.gender.id_for_label }}" class="form-label">
{% trans "Gender" %} <span class="text-danger">*</span> {% trans "Gender" %} <span class="text-danger">*</span>
</label> </label>
@ -215,7 +202,7 @@
<div class="card-footer text-center"> <div class="card-footer text-center">
<small class="text-muted"> <small class="text-muted">
{% trans "Already have an account?" %} {% trans "Already have an account?" %}
<a href="{% url 'portal_login' %}" class="text-decoration-none text-kaauh-teal"> <a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.form_template.slug %}" class="text-decoration-none text-kaauh-teal">
{% trans "Login here" %} {% trans "Login here" %}
</a> </a>
</small> </small>

View File

@ -13,12 +13,17 @@
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary"> <a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit"></i> Edit
</a> </a>
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning"> {% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
<i class="fas fa-key"></i> Generate Keys <i class="fas fa-key"></i> Generate Keys
</a> </a> {% endcomment %}
<button type="button" <button id="toggle-source-status"
class="btn btn-outline-warning" type="button"
class="btn btn-outline-{{ source.is_active|yesno:'warning,success' }}"
hx-post="{% url 'toggle_source_status' source.pk %}" hx-post="{% url 'toggle_source_status' source.pk %}"
hx-target="#toggle-source-status"
hx-select="#toggle-source-status"
hx-select-oob="#source-status"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?" hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
title="{{ source.is_active|yesno:'Deactivate,Activate' }}"> title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i> <i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
@ -93,7 +98,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted">Status</label> <label class="form-label text-muted">Status</label>
<div> <div id="source-status">
{% if source.is_active %} {% if source.is_active %}
<span class="badge bg-success">Active</span> <span class="badge bg-success">Active</span>
{% else %} {% else %}
@ -161,7 +166,7 @@
<div class="card-header"> <div class="card-header">
<h6 class="mb-0">API Credentials</h6> <h6 class="mb-0">API Credentials</h6>
</div> </div>
<div class="card-body"> <div id="api-credentials" class="card-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted">API Key</label> <label class="form-label text-muted">API Key</label>
<div class="input-group"> <div class="input-group">
@ -190,7 +195,7 @@
</div> </div>
</div> </div>
<div class="text-end"> <div class="text-end">
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning btn-sm"> <a hx-post="{% url 'generate_api_keys' source.pk %}" hx-target="#api-credentials" hx-select="#api-credentials" hx-swap="outerHTML" class="btn btn-main-action btn-sm">
<i class="fas fa-key"></i> Generate New Keys <i class="fas fa-key"></i> Generate New Keys
</a> </a>
</div> </div>
@ -371,7 +376,7 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block extra_js %} {% block customJS %}
<script> <script>
function toggleSecretVisibility() { function toggleSecretVisibility() {
const secretInput = document.getElementById('api-secret'); const secretInput = document.getElementById('api-secret');

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static i18n %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
@ -50,7 +50,7 @@
<label for="{{ form.source_type.id_for_label }}" class="form-label"> <label for="{{ form.source_type.id_for_label }}" class="form-label">
{{ form.source_type.label }} <span class="text-danger">*</span> {{ form.source_type.label }} <span class="text-danger">*</span>
</label> </label>
{{ form.source_type|add_class:"form-select" }} {{ form.source_type }}
{% if form.source_type.errors %} {% if form.source_type.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
{% for error in form.source_type.errors %} {% for error in form.source_type.errors %}
@ -63,7 +63,41 @@
</div> </div>
</div> </div>
<div class="mb-3">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.ip_address.label }} <span class="text-danger">*</span>
</label>
{{ form.ip_address|add_class:"form-control" }}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">
{% for error in form.ip_address.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.ip_address.help_text }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.trusted_ips.label }} <span class="text-danger">*</span>
</label>
{{ form.trusted_ips|add_class:"form-control" }}
{% if form.trusted_ips.errors %}
<div class="invalid-feedback d-block">
{% for error in form.trusted_ips.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.trusted_ips.help_text }}</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label"> <label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }} {{ form.description.label }}
</label> </label>
@ -78,23 +112,6 @@
<div class="form-text">{{ form.description.help_text }}</div> <div class="form-text">{{ form.description.help_text }}</div>
</div> </div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.ip_address.label }}
</label>
{{ form.ip_address|add_class:"form-control" }}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">
{% for error in form.ip_address.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.ip_address.help_text }}</div>
</div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
@ -169,8 +186,8 @@
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times"></i> Cancel <i class="fas fa-times"></i> Cancel
</a> </a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-main-action">
<i class="fas fa-save"></i> {{ button_text }} <i class="fas fa-save me-1"></i> {% trans "Save" %}
</button> </button>
</div> </div>
</form> </form>

View File

@ -9,7 +9,7 @@
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Sources</h1> <h1 class="h3 mb-0">Sources</h1>
<a href="{% url 'source_create' %}" class="btn btn-primary"> <a href="{% url 'source_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Create Source <i class="fas fa-plus"></i> Create Source
</a> </a>
</div> </div>
@ -71,12 +71,10 @@
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none"> <a href="{% url 'source_detail' source.pk %}" class="text-decoration-none">
<strong>{{ source.name }}</strong> <strong>{{ source.name }}</strong>
</a> </a>
{% if source.description %}
<br><small class="text-muted">{{ source.description|truncatechars:50 }}</small>
{% endif %}
</td> </td>
<td> <td>
<span class="badge bg-info">{{ source.get_source_type_display }}</span> <span class="badge bg-info">{{ source.source_type }}</span>
</td> </td>
<td> <td>
{% if source.is_active %} {% if source.is_active %}
@ -101,17 +99,17 @@
class="btn btn-sm btn-outline-secondary" title="Edit"> class="btn btn-sm btn-outline-secondary" title="Edit">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" {% comment %} <button type="button"
class="btn btn-sm btn-outline-warning" class="btn btn-sm btn-outline-warning"
hx-post="{% url 'toggle_source_status' source.pk %}" hx-post="{% url 'toggle_source_status' source.pk %}"
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?" hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
title="{{ source.is_active|yesno:'Deactivate,Activate' }}"> title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i> <i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
</button> </button> {% endcomment %}
<a href="{% url 'source_delete' source.pk %}" {% comment %} <a href="{% url 'source_delete' source.pk %}"
class="btn btn-sm btn-outline-danger" title="Delete"> class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a> {% endcomment %}
</div> </div>
</td> </td>
</tr> </tr>
@ -175,7 +173,7 @@
Get started by creating your first source. Get started by creating your first source.
{% endif %} {% endif %}
</p> </p>
<a href="{% url 'source_create' %}" class="btn btn-primary"> <a href="{% url 'source_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Create Source <i class="fas fa-plus"></i> Create Source
</a> </a>
</div> </div>