encryption and settings added

This commit is contained in:
Faheed 2025-12-07 17:02:02 +03:00
parent 934d03b5bb
commit e6d97a44c4
24 changed files with 1186 additions and 335 deletions

View File

@ -62,6 +62,7 @@ INSTALLED_APPS = [
"django_q",
"widget_tweaks",
"easyaudit",
"encrypted_model_fields",
]
@ -531,3 +532,6 @@ LOGGING={
},
}
}
FIELD_ENCRYPTION_KEY="PWQimxxcDjlRsSSof2gaj42a3frmrLt2xgCTa4R06pE="

View File

@ -6,7 +6,7 @@ from .models import (
JobPosting, Application, TrainingMaterial,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview,Person
)
from django.contrib.auth import get_user_model
@ -250,4 +250,5 @@ admin.site.register(ScheduledInterview)
admin.site.register(JobPostingImage)
admin.site.register(Person)
# admin.site.register(User)

View File

@ -6,6 +6,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
def job_not_expired(view_func):
@wraps(view_func)
@ -162,3 +163,12 @@ def staff_or_agency_required(view_func):
def staff_or_candidate_required(view_func):
"""Decorator to restrict view to staff and candidate users."""
return user_type_required(['staff', 'candidate'], login_url='/accounts/login/')(view_func)
def is_superuser(user):
return user.is_authenticated and user.is_superuser
def superuser_required(view_func):
return user_passes_test(is_superuser, login_url='/admin/login/?next=/', redirect_field_name=None)(view_func)

View File

@ -28,7 +28,8 @@ from .models import (
Message,
Person,
Document,
CustomUser
CustomUser,
Interview
)
# from django_summernote.widgets import SummernoteWidget
@ -270,7 +271,7 @@ class SourceAdvancedForm(forms.ModelForm):
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","gender","address"]
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","gpa","national_id","nationality","gender","address"]
widgets = {
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
@ -281,6 +282,8 @@ class PersonForm(forms.ModelForm):
"date_of_birth": forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
"address": forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
"gpa": forms.TextInput(attrs={'class': 'custom-decimal-input'}),
"national_id":forms.NumberInput(attrs={'min': 0, 'step': 1}),
}
class ApplicationForm(forms.ModelForm):
@ -790,11 +793,36 @@ class BulkInterviewTemplateForm(forms.ModelForm):
self.fields["applications"].queryset = Application.objects.filter(
job__slug=slug, stage="Interview"
)
self.fields["topic"].initial = "Interview for " + str(
self.fields["applications"].queryset.first().job.title
)
self.fields["start_date"].initial = timezone.now().date()
working_days_initial = [0, 1, 2, 3, 6] # Monday to Friday
self.fields["working_days"].initial = working_days_initial
self.fields["start_time"].initial = "08:00"
self.fields["end_time"].initial = "14:00"
self.fields["interview_duration"].initial = 30
self.fields["buffer_time"].initial = 10
self.fields["break_start_time"].initial = "11:30"
self.fields["break_end_time"].initial = "12:00"
self.fields["physical_address"].initial = "Airport Road, King Khalid International Airport, Riyadh 11564, Saudi Arabia"
def clean_working_days(self):
working_days = self.cleaned_data.get("working_days")
return [int(day) for day in working_days]
class InterviewCancelForm(forms.ModelForm):
class Meta:
model = Interview
fields = ["cancelled_reason","cancelled_at"]
widgets = {
"cancelled_reason": forms.Textarea(
attrs={"class": "form-control", "rows": 3}
),
"cancelled_at": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
}
class NoteForm(forms.ModelForm):
"""Form for creating and editing meeting comments"""
@ -2292,18 +2320,19 @@ class ApplicantSignupForm(forms.ModelForm):
class Meta:
model = Person
fields = ["first_name","middle_name","last_name", "email","phone","gpa","nationality", "date_of_birth","gender","address"]
fields = ["first_name","middle_name","last_name", "email","phone","gpa","nationality","national_id", "date_of_birth","gender","address"]
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'middle_name': forms.TextInput(attrs={'class': 'form-control'}),
'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': 'custom-decimal-input'}),
"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'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'national_id':forms.NumberInput(attrs={'min': 0, 'step': 1}),
}
def clean(self):
@ -2879,3 +2908,16 @@ class ScheduledInterviewUpdateStatusForm(forms.Form):
class Meta:
model = ScheduledInterview
fields = ['status']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter the choices here
EXCLUDED_STATUS = ScheduledInterview.InterviewStatus.CANCELLED
filtered_choices = [
choice for choice in ScheduledInterview.InterviewStatus.choices
if choice[0]!= EXCLUDED_STATUS
]
# Apply the filtered list back to the field
self.fields['status'].choices = filtered_choices

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-12-02 14:21
# Generated by Django 5.2.7 on 2025-12-07 13:15
import django.contrib.auth.models
import django.contrib.auth.validators
@ -8,6 +8,7 @@ import django.utils.timezone
import django_ckeditor_5.fields
import django_countries.fields
import django_extensions.db.fields
import encrypted_model_fields.fields
import recruitment.validators
from django.conf import settings
from django.db import migrations, models
@ -73,6 +74,8 @@ class Migration(migrations.Migration):
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)),
('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='Cancelled At')),
('cancelled_reason', models.TextField(blank=True, null=True, verbose_name='Cancellation Reason')),
('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')),
('password', models.CharField(blank=True, max_length=20, null=True)),
('zoom_gateway_response', models.JSONField(blank=True, null=True)),
@ -150,10 +153,10 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
('phone', encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone')),
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')),
('email', models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)),
('email', encrypted_model_fields.fields.EncryptedEmailField(error_messages={'unique': 'A user with this email already exists.'}, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
@ -484,13 +487,14 @@ class Migration(migrations.Migration):
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')),
('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
('email', encrypted_model_fields.fields.EncryptedEmailField(db_index=True, unique=True, verbose_name='Email')),
('phone', encrypted_model_fields.fields.EncryptedPositiveIntegerField(blank=True, null=True, verbose_name='Phone')),
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')),
('gpa', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='GPA')),
('gpa', models.DecimalField(decimal_places=2, help_text='GPA must be between 0 and 4.', max_digits=3, verbose_name='GPA')),
('national_id', encrypted_model_fields.fields.EncryptedPositiveIntegerField(help_text='Enter the national id or iqama number')),
('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')),
('address', models.TextField(blank=True, null=True, verbose_name='Address')),
('address', encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Address')),
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-12-07 13:38
import encrypted_model_fields.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='person',
name='address',
field=encrypted_model_fields.fields.EncryptedTextField(blank=True, null=True, verbose_name='Address'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2025-12-07 13:43
import encrypted_model_fields.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_alter_person_address'),
]
operations = [
migrations.AlterField(
model_name='person',
name='national_id',
field=encrypted_model_fields.fields.EncryptedCharField(help_text='Enter the national id or iqama number'),
),
migrations.AlterField(
model_name='person',
name='phone',
field=encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone'),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.2.7 on 2025-12-07 13:59
import encrypted_model_fields.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_alter_person_national_id_alter_person_phone'),
]
operations = [
migrations.AlterField(
model_name='formsubmission',
name='applicant_email',
field=encrypted_model_fields.fields.EncryptedEmailField(blank=True, db_index=True, help_text='Email of the applicant'),
),
migrations.AlterField(
model_name='hiringagency',
name='email',
field=encrypted_model_fields.fields.EncryptedEmailField(blank=True),
),
migrations.AlterField(
model_name='hiringagency',
name='phone',
field=encrypted_model_fields.fields.EncryptedCharField(blank=True),
),
migrations.AlterField(
model_name='participants',
name='email',
field=encrypted_model_fields.fields.EncryptedEmailField(verbose_name='Email'),
),
migrations.AlterField(
model_name='participants',
name='phone',
field=encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone Number'),
),
]

View File

@ -21,6 +21,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Value, IntegerField, CharField,Q
from django.db.models.functions import Coalesce, Cast
from django.db.models.fields.json import KeyTransform, KeyTextTransform
from encrypted_model_fields.fields import EncryptedCharField,EncryptedEmailField,EncryptedTextField
class EmailContent(models.Model):
subject = models.CharField(max_length=255, verbose_name=_("Subject"))
@ -46,8 +47,8 @@ class CustomUser(AbstractUser):
user_type = models.CharField(
max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type")
)
phone = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_("Phone")
phone = EncryptedCharField(
blank=True, null=True, verbose_name=_("Phone")
)
profile_image = models.ImageField(
null=True,
@ -59,7 +60,7 @@ class CustomUser(AbstractUser):
designation = models.CharField(
max_length=100, blank=True, null=True, verbose_name=_("Designation")
)
email = models.EmailField(
email = EncryptedEmailField(
unique=True,
error_messages={
"unique": _("A user with this email already exists."),
@ -73,7 +74,7 @@ class CustomUser(AbstractUser):
@property
def get_unread_message_count(self):
message_list = (
Message.objects.filter(Q(sender=self) | Q(recipient=self), is_read=False)
Message.objects.filter(Q(recipient=self), is_read=False)
)
return message_list.count() or 0
@ -516,13 +517,13 @@ class Person(Base):
middle_name = models.CharField(
max_length=255, blank=True, null=True, verbose_name=_("Middle Name")
)
email = models.EmailField(
email = EncryptedEmailField(
unique=True,
db_index=True,
verbose_name=_("Email"),
)
phone = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_("Phone")
phone = EncryptedCharField(
blank=True, null=True, verbose_name=_("Phone")
)
date_of_birth = models.DateField(
null=True, blank=True, verbose_name=_("Date of Birth")
@ -535,10 +536,13 @@ class Person(Base):
verbose_name=_("Gender"),
)
gpa = models.DecimalField(
max_digits=3, decimal_places=2, blank=True, null=True, verbose_name=_("GPA")
max_digits=3, decimal_places=2, verbose_name=_("GPA"),help_text=_("GPA must be between 0 and 4.")
)
national_id = EncryptedCharField(
help_text=_("Enter the national id or iqama number")
)
nationality = CountryField(blank=True, null=True, verbose_name=_("Nationality"))
address = models.TextField(blank=True, null=True, verbose_name=_("Address"))
address = EncryptedTextField(blank=True, null=True, verbose_name=_("Address"))
# Optional linking to user account
user = models.OneToOneField(
@ -1336,6 +1340,8 @@ class Interview(Base):
default=Status.WAITING,
db_index=True
)
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Cancelled At"))
cancelled_reason = models.TextField(blank=True, null=True, verbose_name=_("Cancellation Reason"))
# Remote-specific (nullable)
meeting_id = models.CharField(
@ -1784,7 +1790,7 @@ class FormSubmission(Base):
applicant_name = models.CharField(
max_length=200, blank=True, help_text="Name of the applicant"
)
applicant_email = models.EmailField(
applicant_email = EncryptedEmailField(
db_index=True, blank=True, help_text="Email of the applicant"
) # Added index
@ -2086,8 +2092,8 @@ class HiringAgency(Base):
contact_person = models.CharField(
max_length=150, blank=True, verbose_name=_("Contact Person")
)
email = models.EmailField(blank=True)
phone = models.CharField(max_length=20, blank=True)
email = EncryptedEmailField(blank=True)
phone = EncryptedCharField(max_length=20, blank=True)
website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
country = CountryField(blank=True, null=True, blank_label=_("Select country"))
@ -2497,8 +2503,8 @@ class Participants(Base):
name = models.CharField(
max_length=255, verbose_name=_("Participant Name"), null=True, blank=True
)
email = models.EmailField(verbose_name=_("Email"))
phone = models.CharField(
email =EncryptedEmailField(verbose_name=_("Email"))
phone = EncryptedCharField(
max_length=12, verbose_name=_("Phone Number"), null=True, blank=True
)
designation = models.CharField(

View File

@ -1,5 +1,5 @@
import logging
import random
from datetime import datetime, timedelta
from django.db import transaction
from django_q.models import Schedule
@ -437,12 +437,8 @@ def notification_created(sender, instance, created, **kwargs):
logger.info(f"Notification cached for SSE: {notification_data}")
def generate_random_password():
import string
return "".join(random.choices(string.ascii_letters + string.digits, k=12))
from .utils import generate_random_password
@receiver(post_save, sender=HiringAgency)
def hiring_agency_created(sender, instance, created, **kwargs):
if created:

View File

@ -283,7 +283,8 @@ urlpatterns = [
name="user_profile_image_update",
),
path("easy_logs/", views.easy_logs, name="easy_logs"),
path("settings/", views.admin_settings, name="admin_settings"),
path('settings/',views.settings,name="settings"),
path("settings/admin/", views.admin_settings, name="admin_settings"),
path("staff/create", views.create_staff_user, name="create_staff_user"),
path(
"set_staff_password/<int:pk>/",
@ -353,6 +354,8 @@ urlpatterns = [
# ),
# Hiring Agency URLs
path("agencies/", views.agency_list, name="agency_list"),
path("regenerate_agency_password/<slug:slug>/", views.regenerate_agency_password, name="regenerate_agency_password"),
path("deactivate_agency/<slug:slug>/", views.deactivate_agency, name="deactivate_agency"),
path("agencies/create/", views.agency_create, name="agency_create"),
path("agencies/<slug:slug>/", views.agency_detail, name="agency_detail"),
path("agencies/<slug:slug>/update/", views.agency_update, name="agency_update"),

View File

@ -9,6 +9,7 @@ from django.utils import timezone
from .models import ScheduledInterview
from django.template.loader import render_to_string
from django.core.mail import send_mail
import random
# nlp = spacy.load("en_core_web_sm")
# def extract_text_from_pdf(pdf_path):
@ -617,3 +618,8 @@ def update_meeting(instance, updated_data):
def generate_random_password():
import string
return "".join(random.choices(string.ascii_letters + string.digits, k=12))

View File

@ -23,6 +23,7 @@ from .decorators import (
StaffRequiredMixin,
StaffOrAgencyRequiredMixin,
StaffOrCandidateRequiredMixin,
superuser_required
)
from .forms import (
StaffUserCreationForm,
@ -38,8 +39,10 @@ from .forms import (
StaffAssignmentForm,
RemoteInterviewForm,
OnsiteInterviewForm,
BulkInterviewTemplateForm
BulkInterviewTemplateForm,
InterviewCancelForm
)
from .utils import generate_random_password
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.http import HttpResponse, JsonResponse
@ -159,8 +162,12 @@ logger = logging.getLogger(__name__)
User = get_user_model()
@login_required
def settings(request):
return render(request,'user/settings.html')
class PersonListView(StaffRequiredMixin, ListView):
class PersonListView(StaffRequiredMixin, ListView,LoginRequiredMixin):
model = Person
template_name = "people/person_list.html"
context_object_name = "people_list"
@ -197,7 +204,7 @@ class PersonListView(StaffRequiredMixin, ListView):
class PersonCreateView(CreateView):
class PersonCreateView(CreateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin):
model = Person
template_name = "people/create_person.html"
form_class = PersonForm
@ -221,13 +228,13 @@ class PersonCreateView(CreateView):
class PersonDetailView(DetailView):
class PersonDetailView(DetailView,LoginRequiredMixin,StaffRequiredMixin):
model = Person
template_name = "people/person_detail.html"
context_object_name = "person"
class PersonUpdateView( UpdateView):
class PersonUpdateView( UpdateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin):
model = Person
template_name = "people/update_person.html"
form_class = PersonForm
@ -239,7 +246,7 @@ class PersonUpdateView( UpdateView):
return redirect("agency_portal_persons_list")
return super().form_valid(form)
class PersonDeleteView(StaffRequiredMixin, DeleteView):
class PersonDeleteView(StaffRequiredMixin, DeleteView,LoginRequiredMixin):
model = Person
template_name = "people/delete_person.html"
success_url = reverse_lazy("person_list")
@ -488,7 +495,9 @@ from django.db.models.functions import Coalesce, Cast # Coalesce handles NULLs
from django.db.models import Avg, IntegerField, Value # Value is used for the default '0'
# These are essential for safely querying PostgreSQL JSONB fields
from django.db.models.fields.json import KeyTransform, KeyTextTransform
@staff_user_required
@login_required
def job_detail(request, slug):
"""View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug)
@ -711,7 +720,8 @@ def job_detail(request, slug):
# )
# return response
@login_required
@staff_user_required
def request_cvs_download(request, slug):
"""
View to initiate the background task.
@ -735,6 +745,8 @@ def request_cvs_download(request, slug):
messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.")
return redirect('job_detail', slug=slug) # Redirect back to the job detail page
@login_required
@staff_user_required
def download_ready_cvs(request, slug):
"""
View to serve the file once it is ready.
@ -985,12 +997,13 @@ def linkedin_callback(request):
# applicant views
def applicant_job_detail(request, slug):
"""View job details for applicants"""
job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE")
return render(request, "jobs/applicant_job_detail.html", {"job": job})
# def applicant_job_detail(request, slug):
# """View job details for applicants"""
# job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE")
# return render(request, "jobs/applicant_job_detail.html", {"job": job})
@login_required
@candidate_user_required
def application_success(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
return render(request, "jobs/application_success.html", {"job": job})
@ -1012,6 +1025,8 @@ def form_builder(request, template_slug=None):
@csrf_exempt
@require_http_methods(["POST"])
@login_required
@staff_user_required
def save_form_template(request):
"""Save a new or existing form template"""
try:
@ -1073,6 +1088,8 @@ def save_form_template(request):
@require_http_methods(["GET"])
@login_required
@staff_user_required
def load_form_template(request, template_slug):
"""Load an existing form template"""
template = get_object_or_404(FormTemplate, slug=template_slug)
@ -1227,7 +1244,8 @@ def delete_form_template(request, template_id):
)
@login_required
@staff_or_candidate_required
def application_submit_form(request, template_slug):
"""Display the form as a step-by-step wizard"""
if not request.user.is_authenticated:
@ -1282,7 +1300,10 @@ def application_submit_form(request, template_slug):
@csrf_exempt
@require_POST
@login_required
@candidate_user_required
def application_submit(request, template_slug):
import re
"""Handle form submission"""
if not request.user.is_authenticated :# or request.user.user_type != "candidate":
return JsonResponse({"success": False, "message": "Unauthorized access."})
@ -1345,6 +1366,29 @@ def application_submit(request, template_slug):
# phone = submission.responses.get(field__label="Phone Number")
# address = submission.responses.get(field__label="Address")
gpa = submission.responses.get(field__label="GPA")
if gpa and gpa.value:
gpa_str = gpa.value.replace("/","").strip()
if not re.match(r'^\d+(\.\d+)?$', gpa_str):
# --- FIX APPLIED HERE ---
return JsonResponse(
{"success": False, "message": _("GPA must be a numeric value.")}
)
try:
gpa_float = float(gpa_str)
except ValueError:
# --- FIX APPLIED HERE ---
return JsonResponse(
{"success": False, "message": _("GPA must be a numeric value.")}
)
if not (0.0 <= gpa_float <= 4.0):
# --- FIX APPLIED HERE ---
return JsonResponse(
{"success": False, "message": _("GPA must be between 0.0 and 4.0.")}
)
resume = submission.responses.get(field__label="Resume Upload")
@ -1450,6 +1494,7 @@ def form_template_all_submissions(request, template_id):
@login_required
@staff_user_required
def form_submission_details(request, template_id, slug):
"""Display detailed view of a specific form submission"""
# Get the form template and verify ownership
@ -1484,6 +1529,8 @@ def form_submission_details(request, template_id, slug):
)
@login_required
@staff_user_required
def _handle_get_request(request, slug, job):
"""
Handles GET requests, setting up forms and restoring candidate selections
@ -1520,7 +1567,8 @@ def _handle_get_request(request, slug, job):
)
@login_required
@staff_user_required
def _handle_preview_submission(request, slug, job):
"""
Handles the initial POST request (Preview Schedule).
@ -1642,6 +1690,8 @@ def _handle_preview_submission(request, slug, job):
)
@login_required
@staff_user_required
def _handle_confirm_schedule(request, slug, job):
"""
Handles the final POST request (Confirm Schedule).
@ -1754,6 +1804,8 @@ def _handle_confirm_schedule(request, slug, job):
return redirect("schedule_interviews", slug=slug)
@login_required
@staff_user_required
def schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST":
@ -1766,6 +1818,8 @@ def schedule_interviews_view(request, slug):
# return redirect("applications_interview_view", slug=slug)
@login_required
@staff_user_required
def confirm_schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST":
@ -1773,6 +1827,7 @@ def confirm_schedule_interviews_view(request, slug):
return _handle_confirm_schedule(request, slug, job)
@login_required
@staff_user_required
def applications_screening_view(request, slug):
"""
@ -1855,6 +1910,7 @@ def applications_screening_view(request, slug):
return render(request, "recruitment/applications_screening_view.html", context)
@login_required
@staff_user_required
def applications_exam_view(request, slug):
"""
@ -1865,6 +1921,7 @@ def applications_exam_view(request, slug):
return render(request, "recruitment/applications_exam_view.html", context)
@login_required
@staff_user_required
def update_application_exam_status(request, slug):
application = get_object_or_404(Application, slug=slug)
@ -1881,7 +1938,7 @@ def update_application_exam_status(request, slug):
{"application": application, "form": form},
)
@login_required
@staff_user_required
def bulk_update_application_exam_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
@ -1901,13 +1958,15 @@ def bulk_update_application_exam_status(request, slug):
return redirect("applications_exam_view", slug=job.slug)
@login_required
@staff_user_required
def application_criteria_view_htmx(request, pk):
application = get_object_or_404(Application, pk=pk)
return render(
request, "includes/application_modal_body.html", {"application": application}
)
@login_required
@staff_user_required
def application_set_exam_date(request, slug):
application = get_object_or_404(Application, slug=slug)
@ -1918,7 +1977,7 @@ def application_set_exam_date(request, slug):
)
return redirect("applications_screening_view", slug=application.job.slug)
@login_required
@staff_user_required
def application_update_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
@ -1998,6 +2057,7 @@ def application_update_status(request, slug):
return response
@login_required
@staff_user_required
def applications_interview_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
@ -2010,6 +2070,7 @@ def applications_interview_view(request, slug):
return render(request, "recruitment/applications_interview_view.html", context)
@login_required
@staff_user_required
def applications_document_review_view(request, slug):
"""
@ -2036,7 +2097,7 @@ def applications_document_review_view(request, slug):
}
return render(request, "recruitment/applications_document_review_view.html", context)
@login_required
@require_POST
@staff_user_required
def reschedule_meeting_for_application(request, slug):
@ -2150,62 +2211,62 @@ def reschedule_meeting_for_application(request, slug):
# @staff_user_required
# def interview_calendar_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
# job = get_object_or_404(JobPosting, slug=slug)
# Get all scheduled interviews for this job
scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
"applicaton", "zoom_meeting"
)
# # Get all scheduled interviews for this job
# scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
# "applicaton", "zoom_meeting"
# )
# Convert interviews to calendar events
events = []
for interview in scheduled_interviews:
# Create start datetime
start_datetime = datetime.combine(
interview.interview_date, interview.interview_time
)
# # Convert interviews to calendar events
# events = []
# for interview in scheduled_interviews:
# # Create start datetime
# start_datetime = datetime.combine(
# interview.interview_date, interview.interview_time
# )
# Calculate end datetime based on interview duration
duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60
end_datetime = start_datetime + timedelta(minutes=duration)
# # Calculate end datetime based on interview duration
# duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60
# end_datetime = start_datetime + timedelta(minutes=duration)
# Determine event color based on status
color = "#00636e" # Default color
if interview.status == "confirmed":
color = "#00a86b" # Green for confirmed
elif interview.status == "cancelled":
color = "#e74c3c" # Red for cancelled
elif interview.status == "completed":
color = "#95a5a6" # Gray for completed
# # Determine event color based on status
# color = "#00636e" # Default color
# if interview.status == "confirmed":
# color = "#00a86b" # Green for confirmed
# elif interview.status == "cancelled":
# color = "#e74c3c" # Red for cancelled
# elif interview.status == "completed":
# color = "#95a5a6" # Gray for completed
events.append(
{
"title": f"Interview: {interview.candidate.name}",
"start": start_datetime.isoformat(),
"end": end_datetime.isoformat(),
"url": f"{request.path}interview/{interview.id}/",
"color": color,
"extendedProps": {
"candidate": interview.candidate.name,
"email": interview.candidate.email,
"status": interview.status,
"meeting_id": interview.zoom_meeting.meeting_id
if interview.zoom_meeting
else None,
"join_url": interview.zoom_meeting.join_url
if interview.zoom_meeting
else None,
},
}
)
# events.append(
# {
# "title": f"Interview: {interview.candidate.name}",
# "start": start_datetime.isoformat(),
# "end": end_datetime.isoformat(),
# "url": f"{request.path}interview/{interview.id}/",
# "color": color,
# "extendedProps": {
# "candidate": interview.candidate.name,
# "email": interview.candidate.email,
# "status": interview.status,
# "meeting_id": interview.zoom_meeting.meeting_id
# if interview.zoom_meeting
# else None,
# "join_url": interview.zoom_meeting.join_url
# if interview.zoom_meeting
# else None,
# },
# }
# )
context = {
"job": job,
"events": events,
"calendar_color": "#00636e",
}
# context = {
# "job": job,
# "events": events,
# "calendar_color": "#00636e",
# }
return render(request, "recruitment/interview_calendar.html", context)
# return render(request, "recruitment/interview_calendar.html", context)
# @staff_user_required
@ -2890,7 +2951,7 @@ def reschedule_meeting_for_application(request, slug):
from django.core.exceptions import ObjectDoesNotExist
@login_required
def user_profile_image_update(request, pk):
user = get_object_or_404(User, pk=pk)
@ -2916,7 +2977,7 @@ def user_profile_image_update(request, pk):
}
return render(request, "user/profile.html", context)
@login_required
def user_detail(request, pk):
user = get_object_or_404(User, pk=pk)
@ -2936,6 +2997,8 @@ def user_detail(request, pk):
return render(request, "user/profile.html", context)
@login_required
@staff_user_required
def easy_logs(request):
"""
Function-based view to display Django Easy Audit logs with tab switching and pagination.
@ -2983,7 +3046,8 @@ def is_superuser_check(user):
return user.is_superuser
@staff_user_required
@login_required
@superuser_required
def create_staff_user(request):
if request.method == "POST":
form = StaffUserCreationForm(request.POST)
@ -3000,13 +3064,15 @@ def create_staff_user(request):
return render(request, "user/create_staff.html", {"form": form})
@staff_user_required
@login_required
@superuser_required
def admin_settings(request):
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)
@login_required
@staff_user_required
def staff_assignment_view(request, slug):
"""
@ -3040,8 +3106,8 @@ def staff_assignment_view(request, slug):
from django.contrib.auth.forms import SetPasswordForm
@staff_user_required
@login_required
@superuser_required
def set_staff_password(request, pk):
user = get_object_or_404(User, pk=pk)
print(request.POST)
@ -3062,8 +3128,8 @@ def set_staff_password(request, pk):
request, "user/staff_password_create.html", {"form": form, "user": user}
)
@staff_user_required
@login_required
@superuser_required
def account_toggle_status(request, pk):
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
@ -3273,6 +3339,7 @@ def zoom_webhook_view(request):
# Hiring Agency CRUD Views
@login_required
@staff_user_required
def agency_list(request):
"""List all hiring agencies with search and pagination"""
@ -3303,6 +3370,8 @@ def agency_list(request):
return render(request, "recruitment/agency_list.html", context)
@login_required
@staff_user_required
def agency_create(request):
"""Create a new hiring agency"""
@ -3325,6 +3394,31 @@ def agency_create(request):
return render(request, "recruitment/agency_form.html", context)
@login_required
@staff_user_required
def regenerate_agency_password(request, slug):
agency=HiringAgency.objects.get(slug=slug)
new_password=generate_random_password()
agency.generated_password=new_password
agency.save()
user=agency.user
user.set_password(new_password)
user.save()
messages.success(request, f'New password generated for agency "{agency.name}" successfully!')
return redirect("agency_detail", slug=agency.slug)
@login_required
@staff_user_required
def deactivate_agency(request, slug):
agency = get_object_or_404(HiringAgency, slug=slug)
agency.is_active = False
agency.save()
messages.success(request, f'Agency "{agency.name}" deactivated successfully!')
return redirect("agency_detail", slug=agency.slug)
@login_required
@staff_user_required
def agency_detail(request, slug):
"""View details of a specific hiring agency"""
@ -3360,7 +3454,7 @@ def agency_detail(request, slug):
}
return render(request, "recruitment/agency_detail.html", context)
@login_required
@staff_user_required
def agency_update(request, slug):
"""Update an existing hiring agency"""
@ -3385,7 +3479,7 @@ def agency_update(request, slug):
}
return render(request, "recruitment/agency_form.html", context)
@login_required
@staff_user_required
def agency_delete(request, slug):
"""Delete a hiring agency"""
@ -3716,7 +3810,7 @@ def agency_delete(request, slug):
# }
# return render(request, 'recruitment/agency_candidates.html', context)
@login_required
@staff_user_required
def agency_applications(request, slug):
"""View all applications from a specific agency"""
@ -3748,6 +3842,7 @@ def agency_applications(request, slug):
# Agency Portal Management Views
@login_required
@staff_user_required
def agency_assignment_list(request):
"""List all agency job assignments"""
@ -3782,6 +3877,7 @@ def agency_assignment_list(request):
return render(request, "recruitment/agency_assignment_list.html", context)
@login_required
@staff_user_required
def agency_assignment_create(request, slug=None):
"""Create a new agency job assignment"""
@ -3820,6 +3916,7 @@ def agency_assignment_create(request, slug=None):
return render(request, "recruitment/agency_assignment_form.html", context)
@login_required
@staff_user_required
def agency_assignment_detail(request, slug):
"""View details of a specific agency assignment"""
@ -3856,7 +3953,7 @@ def agency_assignment_detail(request, slug):
}
return render(request, "recruitment/agency_assignment_detail.html", context)
@login_required
@staff_user_required
def agency_assignment_update(request, slug):
"""Update an existing agency assignment"""
@ -3881,7 +3978,7 @@ def agency_assignment_update(request, slug):
}
return render(request, "recruitment/agency_assignment_form.html", context)
@login_required
@staff_user_required
def agency_access_link_create(request):
"""Create access link for agency assignment"""
@ -3909,6 +4006,7 @@ def agency_access_link_create(request):
return render(request, "recruitment/agency_access_link_form.html", context)
@login_required
@staff_user_required
def agency_access_link_detail(request, slug):
"""View details of an access link"""
@ -3924,7 +4022,7 @@ def agency_access_link_detail(request, slug):
}
return render(request, "recruitment/agency_access_link_detail.html", context)
@login_required
@staff_user_required
def agency_assignment_extend_deadline(request, slug):
"""Extend deadline for an agency assignment"""
@ -4124,6 +4222,7 @@ def applicant_portal_dashboard(request):
@login_required
@candidate_user_required
def applicant_application_detail(request, slug):
"""View detailed information about a specific application"""
if not request.user.is_authenticated:
@ -4176,6 +4275,7 @@ def applicant_application_detail(request, slug):
return render(request, "recruitment/applicant_application_detail.html", context)
@login_required
@agency_user_required
def agency_portal_persons_list(request):
"""Agency portal page showing all persons who come through this agency"""
@ -4231,6 +4331,7 @@ def agency_portal_persons_list(request):
return render(request, "recruitment/agency_portal_persons_list.html", context)
@login_required
@agency_user_required
def agency_portal_dashboard(request):
"""Agency portal dashboard showing all assignments for the agency"""
@ -4288,6 +4389,7 @@ def agency_portal_dashboard(request):
return render(request, "recruitment/agency_portal_dashboard.html", context)
@login_required
@agency_user_required
def agency_portal_submit_application_page(request, slug):
"""Dedicated page for submitting a application """
@ -4354,7 +4456,7 @@ def agency_portal_submit_application_page(request, slug):
}
return render(request, "recruitment/agency_portal_submit_application.html", context)
@login_required
@agency_user_required
def agency_portal_submit_application(request):
"""Handle candidate submission via AJAX (for embedded form)"""
@ -4419,6 +4521,8 @@ def agency_portal_submit_application(request):
return render(request, "recruitment/agency_portal_submit_application.html", context)
@login_required
@staff_or_agency_required
def agency_portal_assignment_detail(request, slug):
"""View details of a specific assignment - routes to admin or agency template"""
assignment = get_object_or_404(
@ -4439,6 +4543,7 @@ def agency_portal_assignment_detail(request, slug):
return redirect("portal_login")
@login_required
@agency_user_required
def agency_assignment_detail_agency(request, slug, assignment_id):
"""Handle agency portal assignment detail view"""
@ -4501,6 +4606,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
return render(request, "recruitment/agency_portal_assignment_detail.html", context)
@login_required
@staff_user_required
def agency_assignment_detail_admin(request, slug):
"""Handle admin assignment detail view"""
@ -4530,6 +4636,7 @@ def agency_assignment_detail_admin(request, slug):
#will check the changes application to appliaction in this function
@login_required
@agency_user_required
def agency_portal_edit_application(request, candidate_id):
"""Edit a candidate for agency portal"""
@ -4591,6 +4698,7 @@ def agency_portal_edit_application(request, candidate_id):
return redirect("agency_portal_dashboard")
@login_required
@agency_user_required
def agency_portal_delete_application(request, candidate_id):
"""Delete a candidate for agency portal"""
@ -4629,7 +4737,7 @@ def agency_portal_delete_application(request, candidate_id):
# Message Views
@login_required
def message_list(request):
"""List all messages for the current user"""
# Get filter parameters
@ -4721,10 +4829,14 @@ def message_create(request):
# Send email if message_type is 'email' and recipient has email
if message.recipient and message.recipient.email:
if request.user.user_type != "staff":
message=message.content
else:
message=message.content.append(f"\n\n Sent by: {request.user.get_full_name()} ({request.user.email})")
try:
email_result = async_task('recruitment.tasks._task_send_individual_email',
subject=message.subject,
body_message=message.content,
body_message=message,
recipient=message.recipient.email,
attachments=None,
sender=False,
@ -5262,6 +5374,7 @@ def portal_logout(request):
# Interview Creation Views
@login_required
@staff_user_required
def interview_create_type_selection(request, application_slug):
"""Show interview type selection page for a application"""
@ -5274,6 +5387,7 @@ def interview_create_type_selection(request, application_slug):
return render(request, 'interviews/interview_create_type_selection.html', context)
@login_required
@staff_user_required
def interview_create_remote(request, application_slug):
"""Create remote interview for a candidate"""
@ -5306,6 +5420,7 @@ def interview_create_remote(request, application_slug):
return render(request, 'interviews/interview_create_remote.html', context)
@login_required
@staff_user_required
def interview_create_onsite(request, application_slug):
"""Create onsite interview for a candidate"""
@ -5344,6 +5459,8 @@ def interview_create_onsite(request, application_slug):
return render(request, 'interviews/interview_create_onsite.html', context)
@login_required
@staff_user_required
def get_interview_list(request, job_slug):
from .forms import ScheduledInterviewUpdateStatusForm
application = Application.objects.get(slug=job_slug)
@ -5351,6 +5468,9 @@ def get_interview_list(request, job_slug):
interview_status_form = ScheduledInterviewUpdateStatusForm()
return render(request, 'interviews/partials/interview_list.html', {'interviews': interviews, 'application': application,'interview_status_form':interview_status_form})
@login_required
@staff_user_required
@require_POST
def update_interview_status(request,slug):
from .forms import ScheduledInterviewUpdateStatusForm
@ -5364,22 +5484,59 @@ def update_interview_status(request,slug):
messages.success(request, "Interview status updated successfully.")
return redirect('interview_detail', slug=slug)
@require_POST
def cancel_interview_for_application(request,slug):
scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
if request.method == 'POST':
if scheduled_interview.interview_type == 'REMOTE':
result = delete_zoom_meeting(scheduled_interview.interview.meeting_id)
if result["status"] != "success":
messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}")
return redirect('interview_detail', slug=slug)
# @require_POST
# def cancel_interview_for_application(request,slug):
# scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
# if request.method == 'POST':
# if scheduled_interview.interview_type == 'REMOTE':
# result = delete_zoom_meeting(scheduled_interview.interview.meeting_id)
# if result["status"] != "success":
# messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}")
# return redirect('interview_detail', slug=slug)
# scheduled_interview.delete()
# messages.success(request, "Interview cancelled successfully.")
# return redirect('interview_list')
@require_POST
@login_required # Assuming this should be protected
@staff_user_required # Assuming only staff can cancel
def cancel_interview_for_application(request, slug):
"""
Handles POST request to cancel an interview, setting the status
and saving the form data (likely a reason for cancellation).
"""
interview = get_object_or_404(Interview, slug=slug)
scheduled_interview = get_object_or_404(ScheduledInterview, interview=interview)
form = InterviewCancelForm(request.POST, instance=interview)
if form.is_valid():
interview.status = interview.Status.CANCELLED
scheduled_interview.status = scheduled_interview.InterviewStatus.CANCELLED
scheduled_interview.save(update_fields=['status'])
interview.save(update_fields=['status']) # Saves the new status
form.save() # Saves form data
messages.success(request, _("Interview cancelled successfully."))
return redirect('interview_detail', slug=scheduled_interview.slug)
else:
error_list = [f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()]
error_message = _("Please correct the following errors: ") + " ".join(error_list)
messages.error(request, error_message)
return redirect('interview_detail', slug=scheduled_interview.slug)
scheduled_interview.delete()
messages.success(request, "Interview cancelled successfully.")
return redirect('interview_list')
@login_required
@staff_user_required
def agency_access_link_deactivate(request, slug):
"""Deactivate an agency access link"""
access_link = get_object_or_404(
@ -5417,6 +5574,7 @@ def agency_access_link_deactivate(request, slug):
@login_required
@staff_user_required
def agency_access_link_reactivate(request, slug):
"""Reactivate an agency access link"""
access_link = get_object_or_404(
@ -5453,6 +5611,7 @@ def agency_access_link_reactivate(request, slug):
return render(request, "recruitment/agency_access_link_confirm.html", context)
@agency_user_required
def api_application_detail(request, candidate_id):
"""API endpoint to get candidate details for agency portal"""
@ -5490,7 +5649,7 @@ def api_application_detail(request, candidate_id):
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
@login_required
@staff_user_required
def compose_application_email(request, job_slug):
"""Compose email to participants about a candidate"""
@ -5625,7 +5784,7 @@ def compose_application_email(request, job_slug):
else:
# GET request - show the form
form = CandidateEmailForm(job, candidates)
form = CandidateEmailForm(job, candidates,request)
return render(
request,
@ -5637,6 +5796,7 @@ def compose_application_email(request, job_slug):
# Source CRUD Views
@login_required
@staff_user_required
def source_list(request):
"""List all sources with search and pagination"""
@ -5666,6 +5826,7 @@ def source_list(request):
return render(request, "recruitment/source_list.html", context)
@login_required
@staff_user_required
def source_create(request):
"""Create a new source"""
@ -5688,6 +5849,7 @@ def source_create(request):
return render(request, "recruitment/source_form.html", context)
@login_required
@staff_user_required
def source_detail(request, slug):
"""View details of a specific source"""
@ -5715,6 +5877,7 @@ def source_detail(request, slug):
return render(request, "recruitment/source_detail.html", context)
@login_required
@staff_user_required
def source_update(request, slug):
"""Update an existing source"""
@ -5740,6 +5903,7 @@ def source_update(request, slug):
return render(request, "recruitment/source_form.html", context)
@login_required
@staff_user_required
def source_delete(request, slug):
"""Delete a source"""
@ -5761,6 +5925,7 @@ def source_delete(request, slug):
@login_required
@staff_user_required
def source_generate_keys(request, slug):
"""Generate new API keys for a source"""
source = get_object_or_404(Source, slug=slug)
@ -5787,6 +5952,7 @@ def source_generate_keys(request, slug):
@login_required
@staff_user_required
def source_toggle_status(request, slug):
"""Toggle active status of a source"""
source = get_object_or_404(Source, slug=slug)
@ -5868,6 +6034,7 @@ def application_signup(request, slug):
# Interview Views
@login_required
@staff_user_required
def interview_list(request):
"""List all interviews with filtering and pagination"""
@ -5906,7 +6073,7 @@ def interview_list(request):
}
return render(request, 'interviews/interview_list.html', context)
@login_required
@staff_user_required
def interview_detail(request, slug):
"""View details of a specific interview"""
@ -5914,12 +6081,15 @@ def interview_detail(request, slug):
interview = get_object_or_404(ScheduledInterview, slug=slug)
reschedule_form = ScheduledInterviewForm()
reschedule_form.initial['topic'] = interview.interview.topic
meeting=interview.interview
context = {
'interview': interview,
'reschedule_form':reschedule_form,
'interview_status_form':ScheduledInterviewUpdateStatusForm()
'interview_status_form':ScheduledInterviewUpdateStatusForm(),
'cancel_form':InterviewCancelForm(instance=meeting),
}
return render(request, 'interviews/interview_detail.html', context)
@ -6603,6 +6773,8 @@ def interview_detail(request, slug):
# return redirect('meeting_details', slug=slug)
@login_required
@staff_user_required
def application_add_note(request, slug):
from .models import Note
from .forms import NoteForm
@ -6631,6 +6803,8 @@ def application_add_note(request, slug):
notes = Note.objects.filter(application=application).order_by('-created_at')
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':application,'notes':notes,'url':url})
@login_required
@staff_user_required
def interview_add_note(request, slug):
from .models import Note
from .forms import NoteForm
@ -6656,3 +6830,25 @@ def interview_add_note(request, slug):
form.fields['author'].widget = HiddenInput()
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()})
# @login_required
# @staff_user_required
# def archieve_application_bank_view(request):
# """View to list all applications in the application bank"""
# applications = Application.objects.filter(stage="Applied").se
# lect_related('person', 'job_posting').all().order_by('-created_at')
# paginator = Paginator(applications, 20) # Show 20 applications per page
# page_number = request.GET.get('page')
# page_obj = paginator.get_page(page_number)
# context = {
# 'page_obj': page_obj,
# 'applications': applications,
# }
# return render(request, 'jobs/archieve_applications_bank.html', context)
# In your views.py (or where the application views are defined)

View File

@ -228,6 +228,8 @@ class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa
slug_url_kwarg = 'slug'
@login_required
@staff_user_required
def retry_scoring_view(request,slug):
from django_q.tasks import async_task
@ -303,6 +305,10 @@ def application_update_stage(request, slug):
messages.success(request,_("application Stage Updated"))
return redirect("application_detail",slug=application.slug)
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.TrainingMaterial
template_name = 'recruitment/training_list.html'
@ -755,7 +761,7 @@ STAGE_CONFIG = {
'headers': ['Name', 'Email', 'Phone', 'Application Date', 'Offer Status', 'Offer Date', 'Match Score', 'Years Experience', 'Professional Category']
},
'hired': {
'filter': {'offer_status': 'Accepted'},
'filter': {'stage': 'Hired'},
'fields': ['name', 'email', 'phone', 'created_at', 'offer_date', 'ai_score', 'years_experience', 'professional_category', 'join_date'],
'headers': ['Name', 'Email', 'Phone', 'Application Date', 'Hire Date', 'Match Score', 'Years Experience', 'Professional Category', 'Join Date']
}

View File

@ -10,6 +10,7 @@ import secrets
import string
from .models import Source, IntegrationLog
from .forms import SourceForm, generate_api_key, generate_api_secret
from .decorators import login_required, staff_user_required
class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
"""List all sources"""
@ -182,6 +183,8 @@ class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
return super().delete(request, *args, **kwargs)
@login_required
@staff_user_required
def generate_api_keys_view(request, pk):
"""Generate new API keys for a specific source"""
if not request.user.is_staff:
@ -228,6 +231,8 @@ def generate_api_keys_view(request, pk):
return JsonResponse({'error': 'Invalid request method'}, status=405)
@login_required
@staff_user_required
def toggle_source_status_view(request, pk):
"""Toggle the active status of a source"""
if not request.user.is_staff:
@ -267,7 +272,7 @@ def toggle_source_status_view(request, pk):
# 'is_active': source.is_active,
# 'message': f'Source "{source.name}" {status_text} successfully'
# })
@login_required
def copy_to_clipboard_view(request):
"""HTMX endpoint to copy text to clipboard"""
if request.method == 'POST':

209
requirements.tx Normal file
View File

@ -0,0 +1,209 @@
amqp==5.3.1
annotated-types==0.7.0
appdirs==1.4.4
arrow==1.3.0
asgiref==3.10.0
asteval==1.0.6
astunparse==1.6.3
attrs==25.3.0
billiard==4.2.2
bleach==6.2.0
blessed==1.22.0
blinker==1.9.0
blis==1.3.0
boto3==1.40.45
botocore==1.40.45
bw-migrations==0.2
bw2data==4.5
bw2parameters==1.1.0
bw_processing==1.0
cached-property==2.0.1
catalogue==2.0.10
celery==5.5.3
certifi==2025.10.5
cffi==2.0.0
channels==4.3.1
chardet==5.2.0
charset-normalizer==3.4.3
click==8.3.0
click-didyoumean==0.3.1
click-plugins==1.1.1.2
click-repl==0.3.0
cloudpathlib==0.22.0
confection==0.1.5
constructive_geometries==1.0
country_converter==1.3.1
crispy-bootstrap5==2025.6
cryptography==46.0.2
cymem==2.0.11
dataflows-tabulator==1.54.3
datapackage==1.15.4
datastar-py==0.6.5
deepdiff==7.0.1
Deprecated==1.2.18
Django==5.2.7
django-allauth==65.12.1
django-ckeditor-5==0.2.18
django-cors-headers==4.9.0
django-countries==7.6.1
django-crispy-forms==2.4
django-easy-audit==1.3.7
django-encrypted-model-fields==0.6.5
django-extensions==4.1
django-filter==25.1
django-js-asset==3.1.2
django-picklefield==3.3
django-q2==1.8.0
django-template-partials==25.2
django-unfold==0.67.0
django-widget-tweaks==1.5.0
django_celery_results==2.6.0
djangorestframework==3.16.1
docopt==0.6.2
dotenv==0.9.9
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
et_xmlfile==2.0.0
Faker==37.8.0
filelock==3.19.1
flexcache==0.3
flexparser==0.4
fsspec==2025.9.0
greenlet==3.2.4
hf-xet==1.1.10
huggingface-hub==0.35.3
idna==3.10
ijson==3.4.0
isodate==0.7.2
Jinja2==3.1.6
jmespath==1.0.1
joblib==1.5.2
jsonlines==4.0.0
jsonpointer==3.0.0
jsonschema==4.25.1
jsonschema-specifications==2025.9.1
kombu==5.5.4
langcodes==3.5.0
language_data==1.3.0
linear-tsv==1.1.0
llvmlite==0.45.1
loguru==0.7.3
lxml==6.0.2
marisa-trie==1.3.1
markdown-it-py==4.0.0
MarkupSafe==3.0.3
matrix_utils==0.6.2
mdurl==0.1.2
morefs==0.2.2
mpmath==1.3.0
mrio-common-metadata==0.2.1
murmurhash==1.0.13
networkx==3.5
numba==0.62.1
numpy==2.3.3
nvidia-cublas-cu12==12.8.4.1
nvidia-cuda-cupti-cu12==12.8.90
nvidia-cuda-nvrtc-cu12==12.8.93
nvidia-cuda-runtime-cu12==12.8.90
nvidia-cudnn-cu12==9.10.2.21
nvidia-cufft-cu12==11.3.3.83
nvidia-cufile-cu12==1.13.1.3
nvidia-curand-cu12==10.3.9.90
nvidia-cusolver-cu12==11.7.3.90
nvidia-cusparse-cu12==12.5.8.93
nvidia-cusparselt-cu12==0.7.1
nvidia-nccl-cu12==2.27.3
nvidia-nvjitlink-cu12==12.8.93
nvidia-nvtx-cu12==12.8.90
openpyxl==3.1.5
ordered-set==4.1.0
packaging==25.0
pandas==2.3.3
pdfminer.six==20250506
pdfplumber==0.11.7
peewee==3.18.2
pillow==11.3.0
Pint==0.25
platformdirs==4.4.0
preshed==3.0.10
prettytable==3.16.0
prompt_toolkit==3.0.52
psycopg==3.2.11
pycparser==2.23
pydantic==2.11.10
pydantic-settings==2.11.0
pydantic_core==2.33.2
pyecospold==4.0.0
Pygments==2.19.2
PyJWT==2.10.1
PyMuPDF==1.26.4
pyparsing==3.2.5
PyPDF2==3.0.1
pypdfium2==4.30.0
PyPrind==2.11.3
pytesseract==0.3.13
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-dotenv==1.1.1
python-json-logger==3.3.0
pytz==2025.2
pyxlsb==1.0.10
PyYAML==6.0.3
randonneur==0.6.2
randonneur_data==0.6.1
RapidFuzz==3.14.1
rdflib==7.2.1
redis==3.5.3
referencing==0.36.2
regex==2025.9.18
requests==2.32.5
rfc3986==2.0.0
rich==14.1.0
rpds-py==0.27.1
s3transfer==0.14.0
safetensors==0.6.2
scikit-learn==1.7.2
scipy==1.16.2
sentence-transformers==5.1.1
setuptools==80.9.0
shellingham==1.5.4
six==1.17.0
smart_open==7.3.1
snowflake-id==1.0.2
spacy==3.8.7
spacy-legacy==3.0.12
spacy-loggers==1.0.5
SPARQLWrapper==2.0.0
sparse==0.17.0
SQLAlchemy==2.0.43
sqlparse==0.5.3
srsly==2.5.1
stats_arrays==0.7
structlog==25.4.0
sympy==1.14.0
tableschema==1.21.0
thinc==8.3.6
threadpoolctl==3.6.0
tokenizers==0.22.1
toolz==1.0.0
torch==2.8.0
tqdm==4.67.1
transformers==4.57.0
triton==3.4.0
typer==0.19.2
types-python-dateutil==2.9.0.20251008
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.2
unicodecsv==0.14.1
urllib3==2.5.0
vine==5.1.0
voluptuous==0.15.2
wasabi==1.1.3
wcwidth==0.2.14
weasel==0.4.1
webencodings==0.5.1
wheel==0.45.1
wrapt==1.17.3
wurst==0.4
xlrd==2.0.2
xlsxwriter==3.2.9

View File

@ -1,10 +1,8 @@
amqp==5.3.1
annotated-types==0.7.0
anthropic==0.63.0
anyio==4.11.0
appdirs==1.4.4
arrow==1.3.0
asgiref==3.9.2
asgiref==3.10.0
asteval==1.0.6
astunparse==1.6.3
attrs==25.3.0
@ -13,8 +11,8 @@ bleach==6.2.0
blessed==1.22.0
blinker==1.9.0
blis==1.3.0
boto3==1.40.37
botocore==1.40.37
boto3==1.40.45
botocore==1.40.45
bw-migrations==0.2
bw2data==4.5
bw2parameters==1.1.0
@ -22,7 +20,8 @@ bw_processing==1.0
cached-property==2.0.1
catalogue==2.0.10
celery==5.5.3
certifi==2025.8.3
certifi==2025.10.5
cffi==2.0.0
channels==4.3.1
chardet==5.2.0
charset-normalizer==3.4.3
@ -35,50 +34,49 @@ confection==0.1.5
constructive_geometries==1.0
country_converter==1.3.1
crispy-bootstrap5==2025.6
cryptography==46.0.2
cymem==2.0.11
dataflows-tabulator==1.54.3
datapackage==1.15.4
datastar-py==0.6.5
deepdiff==7.0.1
Deprecated==1.2.18
distro==1.9.0
Django==5.2.6
django-allauth==65.11.2
Django==5.2.7
django-allauth==65.12.1
django-ckeditor-5==0.2.18
django-cors-headers==4.9.0
django-countries==7.6.1
django-crispy-forms==2.4
django-easy-audit==1.3.7
django-encrypted-model-fields==0.6.5
django-extensions==4.1
django-filter==25.1
django-js-asset==3.1.2
django-picklefield==3.3
django-q2==1.8.0
django-summernote==0.8.20.0
django-template-partials==25.2
django-unfold==0.66.0
django-unfold==0.67.0
django-widget-tweaks==1.5.0
django_celery_results==2.6.0
djangorestframework==3.16.1
docopt==0.6.2
dotenv==0.9.9
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
et_xmlfile==2.0.0
Faker==37.8.0
filelock==3.19.1
flexcache==0.3
flexparser==0.4
fsspec==2025.9.0
gpt-po-translator==1.3.2
greenlet==3.2.4
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
hf-xet==1.1.10
huggingface-hub==0.35.3
idna==3.10
ijson==3.4.0
iniconfig==2.1.0
isodate==0.7.2
isort==5.13.2
Jinja2==3.1.6
jiter==0.11.1
jmespath==1.0.1
joblib==1.5.2
jsonlines==4.0.0
jsonpointer==3.0.0
jsonschema==4.25.1
@ -87,37 +85,52 @@ kombu==5.5.4
langcodes==3.5.0
language_data==1.3.0
linear-tsv==1.1.0
llvmlite==0.45.0
llvmlite==0.45.1
loguru==0.7.3
lxml==6.0.2
marisa-trie==1.3.1
markdown-it-py==4.0.0
MarkupSafe==3.0.2
MarkupSafe==3.0.3
matrix_utils==0.6.2
mdurl==0.1.2
morefs==0.2.2
mpmath==1.3.0
mrio-common-metadata==0.2.1
murmurhash==1.0.13
numba==0.62.0
networkx==3.5
numba==0.62.1
numpy==2.3.3
openai==1.99.9
nvidia-cublas-cu12==12.8.4.1
nvidia-cuda-cupti-cu12==12.8.90
nvidia-cuda-nvrtc-cu12==12.8.93
nvidia-cuda-runtime-cu12==12.8.90
nvidia-cudnn-cu12==9.10.2.21
nvidia-cufft-cu12==11.3.3.83
nvidia-cufile-cu12==1.13.1.3
nvidia-curand-cu12==10.3.9.90
nvidia-cusolver-cu12==11.7.3.90
nvidia-cusparse-cu12==12.5.8.93
nvidia-cusparselt-cu12==0.7.1
nvidia-nccl-cu12==2.27.3
nvidia-nvjitlink-cu12==12.8.93
nvidia-nvtx-cu12==12.8.90
openpyxl==3.1.5
ordered-set==4.1.0
packaging==25.0
pandas==2.3.2
pandas==2.3.3
pdfminer.six==20250506
pdfplumber==0.11.7
peewee==3.18.2
pillow==11.3.0
Pint==0.25
platformdirs==4.4.0
pluggy==1.6.0
polib==1.2.0
preshed==3.0.10
prettytable==3.16.0
prompt_toolkit==3.0.52
psycopg2-binary==2.9.11
pycountry==24.6.1
pydantic==2.11.9
pydantic-settings==2.10.1
psycopg==3.2.11
pycparser==2.23
pydantic==2.11.10
pydantic-settings==2.11.0
pydantic_core==2.33.2
pyecospold==4.0.0
Pygments==2.19.2
@ -125,35 +138,36 @@ PyJWT==2.10.1
PyMuPDF==1.26.4
pyparsing==3.2.5
PyPDF2==3.0.1
pypdfium2==4.30.0
PyPrind==2.11.3
pytest==8.3.4
pytest-django==4.11.1
pytesseract==0.3.13
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-dotenv==1.0.1
python-dotenv==1.1.1
python-json-logger==3.3.0
pytz==2025.2
pyxlsb==1.0.10
PyYAML==6.0.2
PyYAML==6.0.3
randonneur==0.6.2
randonneur_data==0.6
randonneur_data==0.6.1
RapidFuzz==3.14.1
rdflib==7.2.1
redis==3.5.3
referencing==0.36.2
requests==2.32.3
responses==0.25.8
regex==2025.9.18
requests==2.32.5
rfc3986==2.0.0
rich==14.1.0
rpds-py==0.27.1
s3transfer==0.14.0
safetensors==0.6.2
scikit-learn==1.7.2
scipy==1.16.2
sentence-transformers==5.1.1
setuptools==80.9.0
setuptools-scm==8.1.0
shellingham==1.5.4
six==1.17.0
smart_open==7.3.1
sniffio==1.3.1
snowflake-id==1.0.2
spacy==3.8.7
spacy-legacy==3.0.12
@ -165,15 +179,19 @@ sqlparse==0.5.3
srsly==2.5.1
stats_arrays==0.7
structlog==25.4.0
sympy==1.14.0
tableschema==1.21.0
tenacity==9.0.0
thinc==8.3.6
tomli==2.2.1
threadpoolctl==3.6.0
tokenizers==0.22.1
toolz==1.0.0
torch==2.8.0
tqdm==4.67.1
transformers==4.57.0
triton==3.4.0
typer==0.19.2
types-python-dateutil==2.9.0.20251008
typing-inspection==0.4.1
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.2
unicodecsv==0.14.1

View File

@ -632,8 +632,12 @@
}
break;
case 'date':
if (value && !/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
state.fieldErrors[field.id] = 'Please select a valid date';
// Regex for YYYY-MM-DD (ISO standard for <input type="date"> output)
const yyyyMmDdRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
if (value && !yyyyMmDdRegex.test(value)) {
// You might want to update the error message based on the input type
state.fieldErrors[field.id] = 'Please enter a valid date (e.g., YYYY-MM-DD).';
return false;
}
break;
@ -1024,7 +1028,7 @@
}
else if (field.type === 'date') {
const input = document.createElement('input');
input.type = 'month';
input.type = 'date';
input.className = 'form-input';
input.placeholder = field.placeholder || 'Select date';
input.id = `field_${field.id}`;
@ -1265,5 +1269,8 @@
// Start the application
document.addEventListener('DOMContentLoaded', init);
</script>
{% endblock content %}

View File

@ -116,6 +116,7 @@
<i class="fas fa-envelope"></i>
</a>
</li> {% endcomment %}
<li class="nav-item me-2 d-none d-lg-block">
{% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">
@ -135,16 +136,9 @@
</form>
{% endif %}
</li>
{% comment %} <li class="nav-item me-2 d-none d-lg-block">
<a class="nav-link text-white" href="{% url 'message_list' %}">
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
</a>
</li>
{% endcomment %}
<li class="nav-item mx-3 d-none d-lg-block mt-2">
<a href="{% url 'message_list' %}"
class=" btn btn-sm btn-outline-warning position-relative">
<a href="{% url 'message_list' %}" class="btn btn-sm btn-outline-warning position-relative">
<i class="fas fa-envelope me-1"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ request.user.get_unread_message_count }}
@ -160,7 +154,7 @@
aria-expanded="false"
aria-label="{% trans 'Toggle user menu' %}"
data-bs-auto-close="outside"
data-bs-offset="0, 16" {# Vertical offset remains 16px to prevent clipping #}
data-bs-offset="0, 16"
>
{% if user.profile_image %}
<img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
@ -168,16 +162,17 @@
title="{% trans 'Your account' %}">
{% else %}
<div class="profile-avatar" title="{% trans 'Your account' %}">
{{ user.first_name }} {{ user.last_name }}
{% if user.first_name %}
{{ user.first_name|first|capfirst }} {{ user.last_name|first|capfirst }}
{% else %}
{{user.username|first|capfirst}}
{% endif %}
</div>
{% endif %}
</button>
<ul
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
style="min-width: 240px;"
>
<li class="px-4 py-3 ">
<ul class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3" style="min-width: 240px;">
<li class="px-4 py-3">
<div class="d-flex align-items-center">
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
{% if user.profile_image %}
@ -197,8 +192,9 @@
</div>
</div>
</li>
<li><hr class="dropdown-divider my-1"></li>
<div>
<li class="nav-item me-3 dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal d-lg-none">
{% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">
@ -219,21 +215,48 @@
{% endif %}
</li>
<li class="d-lg-none"><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'message_list' %}"> <i class="fas fa-envelope fs-5 me-3"></i> <span>{% trans "Messages" %}</span></a></li>
<li class="d-lg-none">
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'message_list' %}">
<i class="fas fa-envelope fs-5 me-3"></i> <span>{% trans "Messages" %}</span>
</a>
</li>
{% if request.user.is_authenticated %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}">
<i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span>
</a>
</li>
{% if request.user.is_superuser %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Staff Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
{% comment %} <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li> {% endcomment %}
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'settings' %}">
<i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Settings" %}</span>
</a>
</li>
{% endif %}
{% comment %}
{% if request.user.is_superuser %}
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}">
<i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Staff Settings" %}</span>
</a>
</li>
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}">
<i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration Settings" %}</span>
</a>
</li>
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}">
<i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span>
</a>
</li>
{% endif %} {% endcomment %}
{% endif %}
{% comment %} CORRECTED LINKEDIN BLOCK {% endcomment %}
{% if not request.session.linkedin_authenticated %}
{% comment %} {% if not request.session.linkedin_authenticated %}
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
<i class="fab fa-linkedin me-3 text-primary fs-5"></i>
@ -245,19 +268,20 @@
<i class="fab fa-linkedin text-primary me-2"></i>
{% trans "LinkedIn Connected" %}
</li>
{% endif %}
{% endif %} {% endcomment %}
<li><hr class="dropdown-divider my-1"></li>
{% if request.user.is_authenticated %}
<li>
<form method="post" action="{% url 'account_logout'%}" class="d-inline">
{% csrf_token %}
<button
type="submit"
class="dropdown-item py-2 px-4 d-flex align-items-center border-0 bg-transparent text-start"
class="dropdown-item py-2 px-4 d-flex align-items-center border-0 bg-transparent text-start w-100"
aria-label="{% trans 'Sign out' %}"
>
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
<i class="fas fa-sign-out-alt me-3 fs-5" style="color:red;"></i>
<span style="color:red;">{% trans "Sign Out" %}</span>
</button>
</form>

View File

@ -1415,8 +1415,8 @@ const elements = {
</label>
`;
// Add field input based on type
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') {
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' ||field.type === 'date') {
const input = document.createElement('input');
input.type = 'text';
input.className = 'field-input';

View File

@ -150,7 +150,7 @@
{% block content %}
<div class="container-fluid pt-5 pb-5 px-lg-5" style="background-color: var(--color-background-light);">
<h1 class="h3 fw-bold dashboard-header mb-4 px-3">
<i class="fas fa-shield-alt me-2" style="color: var(--color-primary);"></i>{% trans "System Audit Logs" %}
<i class="fas fa-shield-alt me-2" style="color: var(--color-primary);"></i>{% trans "System Activity Logs" %}
</h1>
<div class="alert summary-alert border-start border-5 p-3 mb-5 mx-3" role="alert">

View File

@ -85,8 +85,8 @@
.bg-confirmed { background-color: var(--kaauh-info) !important; color: white; }
.bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; }
.bg-completed { background-color: var(--kaauh-success) !important; color: white; }
.bg-remote { background-color: #007bff !important; color: white; }
.bg-onsite { background-color: #6f42c1 !important; color: white; }
.bg-remote { background-color: #004a53 color: white; }
.bg-onsite { background-color: #00636e !important; color: white; }
/* Timeline Styling */
.timeline {
@ -206,7 +206,6 @@
{% block content %}
<div class="container-fluid py-4">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
@ -224,18 +223,18 @@
<a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
</a>
{% if interview.status != 'cancelled' %}
<button type="button" class="btn btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#statusModal">
<i class="fas fa-redo-alt me-1"></i> {% trans "Update Interview status" %}
</button>
{% endif %}
</div>
</div>
<div class="row">
<!-- Left Column - Candidate & Interview Info -->
<div class="col-lg-8">
<!-- Candidate Information Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4">
<div class="d-flex align-items-start justify-content-between mb-3">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
@ -276,7 +275,7 @@
<p class="mb-2"><strong>{% trans "Department:" %}</strong> {{ interview.job.department }}</p>
<p class="mb-2"><strong>{% trans "Applied Date:" %}</strong> {{ interview.application.created_at|date:"d-m-Y" }}</p>
<p class="mb-0"><strong>{% trans "Current Stage:" %}</strong>
<span class="badge stage-badge stage-{{ interview.application.stage|lower }}">
<span class="badge stage-badge bg-primary-theme">
{{ interview.application.stage }}
</span>
</p>
@ -285,7 +284,6 @@
</div>
</div>
<!-- Interview Details Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
@ -384,7 +382,6 @@
</div>
</div>
<!-- Timeline/History Section -->
<div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-history me-2"></i> {% trans "Interview Timeline" %}
@ -395,13 +392,13 @@
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{% trans "Interview Scheduled" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"d-m-Y" }} {{ interview.interview_time|date:"h:i A" }}</p>
<p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"F j, Y" }} {{ interview.interview_time|date:"h:i A" }}</p>
</div>
<small class="text-muted">{{ interview.interview.created_at|date:"d-m-Y h:i A" }}</small>
<small class="text-muted">{{ interview.interview.created_at|date:"F j,Y h:i A" }}</small>
</div>
</div>
</div>
{% if interview.interview.status == 'CONFIRMED' %}
{% if interview.status == 'confirmed' %}
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
@ -414,7 +411,7 @@
</div>
</div>
{% endif %}
{% if interview.interview.status == 'COMPLETED' %}
{% if interview.status == 'completed' %}
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
@ -427,15 +424,15 @@
</div>
</div>
{% endif %}
{% if interview.interview.status == 'CANCELLED' %}
{% if interview.status == 'cancelled' %}
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{% trans "Interview Cancelled" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview was cancelled" %}</p>
<p class="mb-0 text-muted">{% trans "Interview was cancelled on: " %}{{interview.interview.cancelled_at|date:"F j, Y"}}</p>
</div>
<small class="text-muted">{% trans "Recently" %}</small>
</div>
</div>
</div>
@ -444,15 +441,12 @@
</div>
</div>
<!-- Right Column - Participants & Actions -->
<div class="col-lg-4">
<!-- Participants Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4">
{% comment %} <div class="kaauh-card shadow-sm p-4 mb-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-users me-2"></i> {% trans "Participants" %}
</h5>
<!-- Internal Participants -->
{% if interview.participants.exists %}
<h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6>
{% for participant in interview.participants.all %}
@ -468,7 +462,6 @@
{% endfor %}
{% endif %}
<!-- External Participants -->
{% if interview.system_users.exists %}
<h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6>
{% for user in interview.system_users.all %}
@ -496,49 +489,71 @@
data-bs-target="#participantModal">
<i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %}
</button>
</div>
</div> {% endcomment %}
<!-- Actions Panel -->
<div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-cog me-2"></i> {% trans "Actions" %}
</h5>
<div class="action-buttons">
{% if interview.status != 'CANCELLED' and interview.status != 'COMPLETED' %}
{% if interview.status != 'cancelled' and interview.status != 'COMPLETED' %}
{# 1. Interview is Active (SCHEDULED/CONFIRMED) #}
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
data-bs-target="#rescheduleModal">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
<button type="button" class="btn btn-outline-danger btn-sm"
data-bs-toggle="modal"
data-bs-target="#cancelModal">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
</button>
{% elif interview.status == 'cancelled' %}
{# 2. Interview is CANCELLED (Promote Reschedule) #}
<div class="alert alert-danger w-100 py-2 small" role="alert">
<i class="fas fa-info-circle me-1"></i>
{% trans "Interview is CANCELLED." %}
</div>
<div class="alert alert-info py-2 small" role="alert" style="border-left: 5px solid #00636e; background-color: #f0f8ff;">
{# Use an icon for visual appeal #}
<i class="fas fa-info-circle me-2"></i>
<strong>{% trans "Reason:" %}</strong>
{{ interview.interview.cancelled_reason }}
</div>
{% elif interview.status == 'COMPLETED' %}
{# 3. Interview is COMPLETED (Promote Result Update) #}
<div class="alert alert-success w-100 py-2 small" role="alert">
<i class="fas fa-check-circle me-1"></i>
{% trans "Interview is COMPLETED. Update the final result." %}
</div>
<button type="button" class="btn btn-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#resultModal"
style="width: 100%;">
<i class="fas fa-clipboard-check me-1"></i> {% trans "Update Result" %}
</button>
{% endif %}
<button type="button" class="btn btn-outline-primary btn-sm"
<hr class="w-100 mt-2 mb-2">
<button type="button" class="btn btn-outline-primary btn-sm w-100"
data-bs-toggle="modal"
data-bs-target="#emailModal">
<i class="fas fa-envelope me-1"></i> {% trans "Send Email" %}
</button>
{% if interview.status == 'COMPLETED' %}
<button type="button" class="btn btn-outline-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#resultModal">
<i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Candidate Profile Modal -->
<div class="modal fade modal-xl" id="candidateModal" tabindex="-1" aria-labelledby="candidateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
@ -558,7 +573,6 @@
</div>
</div>
<!-- Participant Modal -->
<div class="modal fade" id="participantModal" tabindex="-1" aria-labelledby="participantModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
@ -574,13 +588,11 @@
<div class="mb-3">
<label for="internal_participants" class="form-label">{% trans "Internal Participants" %}</label>
<select multiple class="form-select" id="internal_participants" name="participants">
<!-- Options will be populated dynamically -->
</select>
</div>
<div class="mb-3">
<label for="external_participants" class="form-label">{% trans "External Participants" %}</label>
<select multiple class="form-select" id="external_participants" name="system_users">
<!-- Options will be populated dynamically -->
</select>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
@ -592,7 +604,6 @@
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content kaauh-card">
@ -617,22 +628,22 @@
<div class="mb-3">
<label for="email_message" class="form-label">{% trans "Message" %}</label>
<textarea class="form-control" id="email_message" name="message" rows="6">
{% trans "Dear" %} {{ interview.application.name }},
{% trans "Dear" %} {{ interview.application.name }},
{% trans "Your interview details are as follows:" %}
{% trans "Your interview details are as follows:" %}
{% trans "Date:" %} {{ interview.interview_date|date:"d-m-Y" }}
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }}
{% trans "Job:" %} {{ interview.job.title }}
{% trans "Date:" %} {{ interview.interview_date|date:"d-m-Y" }}
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }}
{% trans "Job:" %} {{ interview.job.title }}
{% if interview.interview.location_type == 'Remote' %}
{% if interview.interview.location_type == 'Remote' %}
{% trans "This is a remote interview. You will receive the meeting link separately." %}
{% else %}
{% else %}
{% trans "This is an onsite interview. Please arrive 10 minutes early." %}
{% endif %}
{% endif %}
{% trans "Best regards," %}
{% trans "HR Team" %}
{% trans "Best regards," %}
{% trans "HR Team" %}
</textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
@ -644,7 +655,6 @@
</div>
</div>
<!-- Reschedule Modal -->
<div class="modal fade" id="rescheduleModal" tabindex="-1" aria-labelledby="rescheduleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
@ -657,20 +667,9 @@
<div class="modal-body">
<form method="post" action="{% url 'reschedule_meeting_for_application' interview.slug %}">
{% csrf_token %}
{{reschedule_form|crispy}}
{% comment %} <div class="mb-3">
<label for="topic" class="form-label">{% trans "topic" %}</label>
<input type="text" class="form-control" id="topic" name="topic" value="{{interview.interview.topic}}" required>
</div>
<div class="mb-3">
<label for="start_time" class="form-label">{% trans "Start Time" %}</label>
<input type="datetime-local" class="form-control" id="start_time" name="start_time" required>
</div>
<div class="mb-3">
<label for="duration" class="form-label">{% trans "Duration" %}</label>
<input type="number" class="form-control" id="duration" name="duration" required>
</div> {% endcomment %}
<button type="submit" class="btn btn-main-action btn-sm">
{{ reschedule_form|crispy }}
<button type="submit" class="btn btn-main-action btn-sm mt-3">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button>
</form>
@ -679,7 +678,6 @@
</div>
</div>
<!-- Cancel Modal -->
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
@ -690,19 +688,20 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'cancel_interview_for_application' interview.slug %}">
<form method="post" action="{% url 'cancel_interview_for_application' interview.interview.slug %}">
{% csrf_token %}
{{ cancel_form|crispy }}
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Are you sure you want to cancel this interview? This action cannot be undone." %}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
</button>
<div class="d-flex gap-2 justify-content-end">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
</button>
</div>
</form>
</div>
@ -710,7 +709,6 @@
</div>
</div>
<!-- Result Modal -->
<div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
@ -737,7 +735,7 @@
<textarea class="form-control" id="result_notes" name="notes" rows="4"
placeholder="{% trans 'Add interview feedback and notes' %}"></textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
<button type="submit" class="btn btn-main-action btn-sm mt-2">
<i class="fas fa-check me-1"></i> {% trans "Update Result" %}
</button>
</form>
@ -746,7 +744,6 @@
</div>
</div>
<!-- Update Status Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
@ -759,14 +756,14 @@
<div class="modal-body">
<form method="post" action="{% url 'update_interview_status' interview.slug %}">
{% csrf_token %}
{{interview_status_form|crispy}}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
{{ interview_status_form|crispy }}
<div class="d-flex gap-2 mt-3 justify-content-end">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
</button>
</div>
</form>
</div>
@ -780,7 +777,7 @@
<script>
document.addEventListener('DOMContentLoaded', function () {
// Clear modal content when hidden
const modals = ['candidateModal', 'participantModal', 'emailModal', 'rescheduleModal', 'cancelModal', 'resultModal'];
const modals = ['candidateModal', 'participantModal', 'emailModal', 'rescheduleModal', 'cancelModal', 'resultModal', 'statusModal'];
modals.forEach(modalId => {
const modal = document.getElementById(modalId);
@ -788,6 +785,7 @@ document.addEventListener('DOMContentLoaded', function () {
modal.addEventListener('hidden.bs.modal', function () {
const modalBody = modal.querySelector('.modal-body');
if (modalBody && modalId === 'candidateModal') {
// Reset content for AJAX-loaded Candidate Modal
modalBody.innerHTML = `
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
@ -795,6 +793,9 @@ document.addEventListener('DOMContentLoaded', function () {
</div>
`;
}
// Note: For forms using Django/Crispy, you might need extra JS
// to explicitly reset form fields if the form is not reloading
// when the modal is closed.
});
}
});

View File

@ -509,6 +509,17 @@
</div>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<a href="{% url 'regenerate_agency_password' agency.slug %}"
class="btn btn-danger"
title="{% trans 'This action resets the agency\'s current login password.' %}"
onclick="return confirm('{% trans "Are you sure you want to regenerate the password for this agency? This cannot be undone." %}');"
>
<i class="fas fa-key me-2"></i>
{% trans "Regenerate Agency Password" %}
</a>
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,220 @@
{% extends 'base.html' %}
{% load static i18n crispy_forms_tags %}
{% block title %}{% trans "System Settings" %} - ATS{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* Main Container & Card Styling */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Button Styling */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Tabs Styling */
.nav-tabs {
border-bottom: 1px solid var(--kaauh-border);
margin-bottom: 1.5rem;
}
.nav-tabs .nav-link {
color: var(--kaauh-primary-text);
border: 1px solid transparent;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
font-weight: 600;
}
.nav-tabs .nav-link.active {
color: var(--kaauh-teal-dark);
background-color: white;
border-color: var(--kaauh-border) var(--kaauh-border) white var(--kaauh-border);
border-top: 3px solid var(--kaauh-teal); /* Active tab indicator */
}
/* Settings Group Styling */
.settings-group {
border-bottom: 1px solid var(--kaauh-border);
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
}
.settings-group:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.setting-action-btn {
margin-top: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="row my-4 mx-4">
<div class="col-md-4 mb-4">
<a href="#integration-settings-page" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fas fa-plug fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "Integration Settings" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Connect and manage external services like Zoom, email providers, and third-party tools." %}
</p>
</div>
</div>
<div class="text-end mt-3">
<span class="btn btn-sm btn-outline-secondary">
{% trans "Go to Settings" %} <i class="fas fa-arrow-right ms-1"></i>
</span>
</div>
</div>
</a>
</div>
<div class="col-md-4 mb-4">
<a href="{% url 'source_list' %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fas fa-sitemap fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "Source & Sync Settings" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Configure automated syncs with job boards and external talent databases (CRM)." %}
</p>
</div>
</div>
<div class="text-end mt-3">
<span class="btn btn-sm btn-outline-secondary">
{% trans "Go to Settings" %} <i class="fas fa-arrow-right ms-1"></i>
</span>
</div>
</div>
</a>
</div>
<div class="col-md-4 mb-4">
<a href="{% url 'admin_settings' %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fas fa-user-friends fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "Staff & Access Settings" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Manage user accounts, define roles, and control system permissions." %}
</p>
</div>
</div>
<div class="text-end mt-3">
<span class="btn btn-sm btn-outline-secondary">
{% trans "Go to Settings" %} <i class="fas fa-arrow-right ms-1"></i>
</span>
</div>
</div>
</a>
</div>
<div class="col-md-4 mb-4">
<a href="{% url "easy_logs" %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fas fa-file-alt fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "System Activity logs" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Check the complete activity of your system here." %}
</p>
</div>
</div>
<div class="text-end mt-3">
<button class="btn btn-sm btn-outline-secondary">
{% trans "Go to Logs" %}<i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</a>
</div>
<div class="col-md-4 mb-4">
<a href="#linkedin-connect-action" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fab fa-linkedin fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "LinkedIn Integration" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Connect the ATS with your LinkedIn Recruiter account to enable profile sourcing." %}
</p>
</div>
</div>
<div class="text-end mt-3">
{% if not request.session.linkedin_authenticated %}
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
<i class="fab fa-linkedin me-3 text-primary fs-5"></i>
<span>{% trans "Connect to LinkedIn" %}</span>
</a>
{% else %}
<p class="text-primary">
<i class="fab fa-linkedin me-2"></i>
{% trans "LinkedIn Connected" %}
</p>
{% endif %}
</div>
</div>
</a>
</div>
</div>
{% endblock %}