Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend
This commit is contained in:
commit
b4b56d8a9d
6
.env
6
.env
@ -1,3 +1,3 @@
|
||||
DB_NAME=haikal_db
|
||||
DB_USER=faheed
|
||||
DB_PASSWORD=Faheed@215
|
||||
DB_NAME=norahuniversity
|
||||
DB_USER=norahuniversity
|
||||
DB_PASSWORD=norahuniversity
|
||||
@ -487,3 +487,6 @@ MESSAGE_TAGS = {
|
||||
|
||||
# Custom User Model
|
||||
AUTH_USER_MODEL = "recruitment.CustomUser"
|
||||
|
||||
|
||||
ZOOM_WEBHOOK_API_KEY = "2GNDC5Rvyw9AHoGikHXsQB"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -55,7 +55,7 @@ class SourceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Source
|
||||
fields = ["name", "source_type", "description", "ip_address", "is_active"]
|
||||
fields = ["name", "source_type", "description", "ip_address","trusted_ips", "is_active"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(
|
||||
attrs={
|
||||
@ -81,6 +81,9 @@ class SourceForm(forms.ModelForm):
|
||||
"ip_address": forms.TextInput(
|
||||
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"}),
|
||||
}
|
||||
|
||||
@ -2228,7 +2231,7 @@ class CandidateSignupForm(forms.ModelForm):
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'email': forms.EmailInput(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'}),
|
||||
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'gender': forms.Select(attrs={'class': 'form-control'}),
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -99,9 +99,9 @@ class JobPosting(Base):
|
||||
# Core Fields
|
||||
title = models.CharField(max_length=200)
|
||||
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(
|
||||
max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE"
|
||||
max_length=20, choices=WORKPLACE_TYPES, default="On-site"
|
||||
)
|
||||
|
||||
# Location
|
||||
|
||||
@ -18,7 +18,9 @@ from .models import (
|
||||
Notification,
|
||||
HiringAgency,
|
||||
Person,
|
||||
Source,
|
||||
)
|
||||
from .forms import generate_api_key, generate_api_secret
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -29,11 +31,10 @@ User = get_user_model()
|
||||
@receiver(post_save, sender=JobPosting)
|
||||
def format_job(sender, instance, created, **kwargs):
|
||||
if created or not instance.ai_parsed:
|
||||
try:
|
||||
form_template = instance.form_template
|
||||
except FormTemplate.DoesNotExist:
|
||||
form = getattr(instance, "form_template", None)
|
||||
if not form:
|
||||
FormTemplate.objects.get_or_create(
|
||||
job=instance, is_active=False, name=instance.title
|
||||
job=instance, is_active=True, name=instance.title
|
||||
)
|
||||
async_task(
|
||||
"recruitment.tasks.format_job_description",
|
||||
@ -469,3 +470,27 @@ def person_created(sender, instance, created, **kwargs):
|
||||
)
|
||||
instance.user = user
|
||||
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")
|
||||
|
||||
@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
OPENROUTER_API_KEY ='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'
|
||||
# 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 = '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.
|
||||
It handles: created, updated, started, ended, and deleted events.
|
||||
"""
|
||||
print(payload)
|
||||
event_type = payload.get('event')
|
||||
object_data = payload['payload']['object']
|
||||
|
||||
@ -534,7 +535,9 @@ def handle_zoom_webhook_event(payload):
|
||||
# elif event_type == 'meeting.updated':
|
||||
# Only update time fields if they are in the payload
|
||||
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.timezone = object_data.get('timezone', meeting_instance.timezone)
|
||||
|
||||
|
||||
@ -1336,6 +1336,7 @@ def application_submit(request, template_slug):
|
||||
# email = submission.responses.get(field__label="Email Address")
|
||||
# phone = submission.responses.get(field__label="Phone Number")
|
||||
# address = submission.responses.get(field__label="Address")
|
||||
gpa = submission.responses.get(field__label="GPA")
|
||||
|
||||
resume = submission.responses.get(field__label="Resume Upload")
|
||||
|
||||
@ -1346,6 +1347,8 @@ def application_submit(request, template_slug):
|
||||
submission.save()
|
||||
# time=timezone.now()
|
||||
person = request.user.person_profile
|
||||
person.gpa = gpa.value if gpa else None
|
||||
person.save()
|
||||
Application.objects.create(
|
||||
person = person,
|
||||
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")
|
||||
screening_rating = request.GET.get("screening_rating")
|
||||
tier1_count_str = request.GET.get("tier1_count")
|
||||
gpa = request.GET.get("gpa")
|
||||
gpa = request.GET.get("GPA")
|
||||
|
||||
try:
|
||||
# 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:
|
||||
candidates = candidates.filter(
|
||||
person__gpa = gpa
|
||||
person__gpa__gt= gpa
|
||||
)
|
||||
print(candidates)
|
||||
|
||||
if tier1_count > 0:
|
||||
candidates = candidates[:tier1_count]
|
||||
@ -3018,7 +3022,6 @@ def is_superuser_check(user):
|
||||
def create_staff_user(request):
|
||||
if request.method == "POST":
|
||||
form = StaffUserCreationForm(request.POST)
|
||||
print(form)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
@ -3034,7 +3037,7 @@ def create_staff_user(request):
|
||||
|
||||
@staff_user_required
|
||||
def admin_settings(request):
|
||||
staffs = User.objects.filter(is_superuser=False)
|
||||
staffs = User.objects.filter(user_type="staff",is_superuser=False)
|
||||
form = ToggleAccountForm()
|
||||
context = {"staffs": staffs, "form": form}
|
||||
return render(request, "user/admin_settings.html", context)
|
||||
@ -3097,12 +3100,11 @@ def account_toggle_status(request, pk):
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@staff_user_required
|
||||
def zoom_webhook_view(request):
|
||||
print(request.headers)
|
||||
print(settings.ZOOM_WEBHOOK_API_KEY)
|
||||
# if api_key != settings.ZOOM_WEBHOOK_API_KEY:
|
||||
# return HttpResponse(status=405)
|
||||
api_key = request.headers.get("X-Zoom-API-KEY")
|
||||
if api_key != settings.ZOOM_WEBHOOK_API_KEY:
|
||||
return HttpResponse(status=405)
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
@ -5497,7 +5499,7 @@ def candidate_signup(request, slug):
|
||||
gender = form.cleaned_data["gender"]
|
||||
nationality = form.cleaned_data["nationality"]
|
||||
address = form.cleaned_data["address"]
|
||||
gpa = form.cleaned_data["gpa"]
|
||||
# gpa = form.cleaned_data["gpa"]
|
||||
password = form.cleaned_data["password"]
|
||||
|
||||
user = User.objects.create_user(
|
||||
@ -5512,7 +5514,7 @@ def candidate_signup(request, slug):
|
||||
phone=phone,
|
||||
gender=gender,
|
||||
nationality=nationality,
|
||||
gpa=gpa,
|
||||
# gpa=gpa,
|
||||
address=address,
|
||||
user = user
|
||||
)
|
||||
|
||||
@ -204,26 +204,27 @@ def generate_api_keys_view(request, pk):
|
||||
source.save()
|
||||
|
||||
# Log the key regeneration
|
||||
IntegrationLog.objects.create(
|
||||
source=source,
|
||||
action=IntegrationLog.ActionChoices.CREATE,
|
||||
endpoint=f'/api/sources/{source.pk}/generate-keys/',
|
||||
method='POST',
|
||||
request_data={
|
||||
'name': source.name,
|
||||
'old_api_key': old_api_key[:8] + '...' if old_api_key else None,
|
||||
'new_api_key': new_api_key[:8] + '...'
|
||||
},
|
||||
ip_address=request.META.get('REMOTE_ADDR'),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
||||
)
|
||||
# IntegrationLog.objects.create(
|
||||
# source=source,
|
||||
# action=IntegrationLog.ActionChoices.CREATE,
|
||||
# endpoint=f'/api/sources/{source.pk}/generate-keys/',
|
||||
# method='POST',
|
||||
# request_data={
|
||||
# 'name': source.name,
|
||||
# 'old_api_key': old_api_key[:8] + '...' if old_api_key else None,
|
||||
# 'new_api_key': new_api_key[:8] + '...'
|
||||
# },
|
||||
# ip_address=request.META.get('REMOTE_ADDR'),
|
||||
# user_agent=request.META.get('HTTP_USER_AGENT', '')
|
||||
# )
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'api_key': new_api_key,
|
||||
'api_secret': new_api_secret,
|
||||
'message': 'API keys regenerated successfully'
|
||||
})
|
||||
return redirect('source_detail', pk=source.pk)
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'api_key': new_api_key,
|
||||
# 'api_secret': new_api_secret,
|
||||
# 'message': 'API keys regenerated successfully'
|
||||
# })
|
||||
|
||||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||||
|
||||
@ -244,27 +245,28 @@ def toggle_source_status_view(request, pk):
|
||||
source.save()
|
||||
|
||||
# Log the status change
|
||||
IntegrationLog.objects.create(
|
||||
source=source,
|
||||
action=IntegrationLog.ActionChoices.SYNC,
|
||||
endpoint=f'/api/sources/{source.pk}/toggle-status/',
|
||||
method='POST',
|
||||
request_data={
|
||||
'name': source.name,
|
||||
'old_status': old_status,
|
||||
'new_status': source.is_active
|
||||
},
|
||||
ip_address=request.META.get('REMOTE_ADDR'),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
||||
)
|
||||
# IntegrationLog.objects.create(
|
||||
# source=source,
|
||||
# action=IntegrationLog.ActionChoices.SYNC,
|
||||
# endpoint=f'/api/sources/{source.pk}/toggle-status/',
|
||||
# method='POST',
|
||||
# request_data={
|
||||
# 'name': source.name,
|
||||
# 'old_status': old_status,
|
||||
# 'new_status': source.is_active
|
||||
# },
|
||||
# ip_address=request.META.get('REMOTE_ADDR'),
|
||||
# user_agent=request.META.get('HTTP_USER_AGENT', '')
|
||||
# )
|
||||
|
||||
status_text = 'activated' if source.is_active else 'deactivated'
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'is_active': source.is_active,
|
||||
'message': f'Source "{source.name}" {status_text} successfully'
|
||||
})
|
||||
return redirect('source_detail', pk=source.pk)
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'is_active': source.is_active,
|
||||
# 'message': f'Source "{source.name}" {status_text} successfully'
|
||||
# })
|
||||
|
||||
def copy_to_clipboard_view(request):
|
||||
"""HTMX endpoint to copy text to clipboard"""
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
--gray-text: #6c757d;
|
||||
--kaauh-border: #d0d7de; /* Cleaner border color */
|
||||
--kaauh-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); /* Deeper shadow for premium look */
|
||||
--kaauh-dark-bg: #0d0d0d;
|
||||
--kaauh-dark-contrast: #1c1c1c;
|
||||
--kaauh-dark-bg: #0d0d0d;
|
||||
--kaauh-dark-contrast: #1c1c1c;
|
||||
|
||||
/* CALCULATED STICKY HEIGHTS (As provided in base) */
|
||||
--navbar-height: 56px;
|
||||
@ -43,17 +43,145 @@
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f0f5;
|
||||
background-color: #f0f0f5;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.text-primary-theme { color: var(--kaauh-teal) !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 {
|
||||
background-color: #00636e;
|
||||
}
|
||||
|
||||
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
@ -67,7 +195,7 @@
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
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) */
|
||||
@ -75,16 +203,16 @@
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, var(--kaauh-dark-contrast) 0%, var(--kaauh-dark-bg) 100%);
|
||||
padding: 4rem 0; /* Reduced from 8rem to 4rem */
|
||||
margin-top: -1px;
|
||||
color: white;
|
||||
position: relative;
|
||||
margin-top: -1px;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero-title {
|
||||
font-size: 2.5rem; /* Reduced from 3.5rem to 2.5rem */
|
||||
font-weight: 800; /* Extra bold */
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.05em;
|
||||
letter-spacing: -0.05em;
|
||||
max-width: 900px;
|
||||
}
|
||||
.hero-section .lead {
|
||||
@ -104,7 +232,7 @@
|
||||
padding: 10rem 0;
|
||||
}
|
||||
.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 */
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
|
||||
.job-listing-card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-left: 6px solid var(--kaauh-teal);
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-left: 6px solid var(--kaauh-teal);
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem !important;
|
||||
padding: 2rem !important;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); /* Lighter default shadow */
|
||||
}
|
||||
.job-listing-card:hover {
|
||||
transform: translateY(-3px); /* Increased lift */
|
||||
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 {
|
||||
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 %}
|
||||
<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">
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
<span class="flag-emoji">🇺🇸</span>
|
||||
<span class="language-text">English</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
@ -224,7 +353,8 @@
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
|
||||
<span class="me-2">🇸🇦</span> العربية (Arabic)
|
||||
<span class="flag-emoji">🇸🇦</span>
|
||||
<span class="language-text">العربية (Arabic)</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
@ -239,7 +369,7 @@
|
||||
<div class="container message-container mt-3">
|
||||
<div class="row">
|
||||
{# 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 %}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i> {{ message }}
|
||||
@ -254,20 +384,20 @@
|
||||
{# ================================================= #}
|
||||
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
|
||||
{# ================================================= #}
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
{% block customJS %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -250,7 +250,7 @@
|
||||
<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" %}
|
||||
{% trans "Applicant" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -272,7 +272,7 @@
|
||||
<th scope="col" rowspan="2">{% trans "Actions" %}</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" %}
|
||||
</th>
|
||||
</tr>
|
||||
@ -282,10 +282,11 @@
|
||||
<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 "Interview" %}</th>
|
||||
<th style="width: calc(50% / 7 * 2);">{% trans "Documets Review" %}</th>
|
||||
<th style="width: calc(50% / 7);">{% trans "Offer" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
@ -311,7 +312,7 @@
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% 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>
|
||||
</a>
|
||||
<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-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_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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
</a>
|
||||
|
||||
{% 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 %}
|
||||
<a href="{% url 'candidate_exam_view' job.slug %}"
|
||||
@ -127,7 +127,7 @@
|
||||
</a>
|
||||
|
||||
{% 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 %}
|
||||
<a href="{% url 'candidate_interview_view' job.slug %}"
|
||||
|
||||
@ -152,10 +152,10 @@
|
||||
<!-- 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" %}
|
||||
<i class="fas fa-user-friends me-2"></i> {% trans "Applicants List" %}
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -168,7 +168,7 @@
|
||||
<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...' %}"
|
||||
placeholder="{% trans 'Search applicant...' %}"
|
||||
value="{{ request.GET.q }}">
|
||||
<button class="btn btn-main-action" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
|
||||
@ -89,6 +89,7 @@
|
||||
|
||||
.status-applied { background: #e3f2fd; color: #1976d2; }
|
||||
.status-screening { background: #fff3e0; color: #f57c00; }
|
||||
.status-document_review { background: #f3e5f5; color: #7b1fa2; }
|
||||
.status-exam { background: #f3e5f5; color: #7b1fa2; }
|
||||
.status-interview { background: #e8f5e8; color: #388e3c; }
|
||||
.status-offer { background: #fff8e1; color: #f9a825; }
|
||||
@ -119,6 +120,9 @@
|
||||
background-color: #f3e5f5;
|
||||
border-color: #ce93d8;
|
||||
}
|
||||
.card-header{
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -155,7 +159,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
@ -172,19 +176,9 @@
|
||||
<div class="progress-label">{% trans "Applied" %}</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 -->
|
||||
{% if application.stage in 'Exam,Interview,Offer,Hired,Rejected' %}
|
||||
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}">
|
||||
{% if application.stage in 'Exam,Interview,Document Review,Offer,Hired,Rejected' %}
|
||||
<div class="progress-step {% if application.stage not in 'Applied,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}">
|
||||
<div class="progress-icon">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
</div>
|
||||
@ -193,8 +187,8 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Interview Stage - Show if current stage is Interview or beyond -->
|
||||
{% if application.stage in 'Interview,Offer,Hired,Rejected' %}
|
||||
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam,Interview' %}completed{% elif application.stage == 'Interview' %}active{% endif %}">
|
||||
{% if application.stage in 'Interview,Document Review,Offer,Hired,Rejected' %}
|
||||
<div class="progress-step {% if application.stage not in 'Applied,Exam,Interview' %}completed{% elif application.stage == 'Interview' %}active{% endif %}">
|
||||
<div class="progress-icon">
|
||||
<i class="fas fa-video"></i>
|
||||
</div>
|
||||
@ -202,9 +196,19 @@
|
||||
</div>
|
||||
{% 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 -->
|
||||
{% 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">
|
||||
<i class="fas fa-handshake"></i>
|
||||
</div>
|
||||
@ -525,6 +529,11 @@
|
||||
<i class="fas fa-search me-2"></i>
|
||||
{% trans "Your application is currently under screening. We are evaluating your qualifications against the job requirements." %}
|
||||
</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' %}
|
||||
<div class="alert alert-purple">
|
||||
<i class="fas fa-clipboard-check me-2"></i>
|
||||
|
||||
@ -647,30 +647,30 @@
|
||||
<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>
|
||||
<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" %}
|
||||
</a>
|
||||
<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?" %}')">
|
||||
</a> {% endcomment %}
|
||||
{% 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" %}
|
||||
</a>
|
||||
</a> {% endcomment %}
|
||||
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
{% 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>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a>
|
||||
</a> {% endcomment %}
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</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>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a>
|
||||
</a> {% endcomment %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -252,7 +252,7 @@
|
||||
{% trans "GPA" %}
|
||||
</label>
|
||||
<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;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<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">
|
||||
{% trans "First Name" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -81,7 +81,19 @@
|
||||
{% endif %}
|
||||
</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">
|
||||
{% trans "Last Name" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -95,19 +107,7 @@
|
||||
</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">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.phone.id_for_label }}" class="form-label">
|
||||
{% trans "Phone Number" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -118,21 +118,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</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">
|
||||
{% trans "Nationality" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -144,7 +131,7 @@
|
||||
{% endif %}
|
||||
</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">
|
||||
{% trans "Gender" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -215,7 +202,7 @@
|
||||
<div class="card-footer text-center">
|
||||
<small class="text-muted">
|
||||
{% 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" %}
|
||||
</a>
|
||||
</small>
|
||||
|
||||
@ -13,12 +13,17 @@
|
||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</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
|
||||
</a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-warning"
|
||||
</a> {% endcomment %}
|
||||
<button id="toggle-source-status"
|
||||
type="button"
|
||||
class="btn btn-outline-{{ source.is_active|yesno:'warning,success' }}"
|
||||
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?"
|
||||
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
|
||||
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||
@ -93,7 +98,7 @@
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Status</label>
|
||||
<div>
|
||||
<div id="source-status">
|
||||
{% if source.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
@ -161,7 +166,7 @@
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">API Credentials</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="api-credentials" class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">API Key</label>
|
||||
<div class="input-group">
|
||||
@ -190,7 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@ -371,7 +376,7 @@
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function toggleSecretVisibility() {
|
||||
const secretInput = document.getElementById('api-secret');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static i18n %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
@ -50,7 +50,7 @@
|
||||
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
||||
{{ form.source_type.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.source_type|add_class:"form-select" }}
|
||||
{{ form.source_type }}
|
||||
{% if form.source_type.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.source_type.errors %}
|
||||
@ -63,7 +63,41 @@
|
||||
</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">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
@ -78,23 +112,6 @@
|
||||
<div class="form-text">{{ form.description.help_text }}</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="mb-3">
|
||||
<div class="form-check">
|
||||
@ -169,8 +186,8 @@
|
||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> {{ button_text }}
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@ -71,12 +71,10 @@
|
||||
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none">
|
||||
<strong>{{ source.name }}</strong>
|
||||
</a>
|
||||
{% if source.description %}
|
||||
<br><small class="text-muted">{{ source.description|truncatechars:50 }}</small>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if source.is_active %}
|
||||
@ -101,17 +99,17 @@
|
||||
class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button"
|
||||
{% comment %} <button type="button"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
hx-post="{% url 'toggle_source_status' source.pk %}"
|
||||
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
|
||||
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
|
||||
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||
</button>
|
||||
<a href="{% url 'source_delete' source.pk %}"
|
||||
</button> {% endcomment %}
|
||||
{% comment %} <a href="{% url 'source_delete' source.pk %}"
|
||||
class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</a> {% endcomment %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -175,7 +173,7 @@
|
||||
Get started by creating your first source.
|
||||
{% endif %}
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user