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", "django_q",
"widget_tweaks", "widget_tweaks",
"easyaudit", "easyaudit",
"encrypted_model_fields",
] ]
@ -530,4 +531,7 @@ LOGGING={
"style": "{", "style": "{",
}, },
} }
} }
FIELD_ENCRYPTION_KEY="PWQimxxcDjlRsSSof2gaj42a3frmrLt2xgCTa4R06pE="

View File

@ -6,7 +6,7 @@ from .models import (
JobPosting, Application, TrainingMaterial, JobPosting, Application, TrainingMaterial,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note, SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview,Person
) )
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -250,4 +250,5 @@ admin.site.register(ScheduledInterview)
admin.site.register(JobPostingImage) admin.site.register(JobPostingImage)
admin.site.register(Person)
# admin.site.register(User) # 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.contrib.auth.mixins import AccessMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
def job_not_expired(view_func): def job_not_expired(view_func):
@wraps(view_func) @wraps(view_func)
@ -162,3 +163,12 @@ def staff_or_agency_required(view_func):
def staff_or_candidate_required(view_func): def staff_or_candidate_required(view_func):
"""Decorator to restrict view to staff and candidate users.""" """Decorator to restrict view to staff and candidate users."""
return user_type_required(['staff', 'candidate'], login_url='/accounts/login/')(view_func) 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, Message,
Person, Person,
Document, Document,
CustomUser CustomUser,
Interview
) )
# from django_summernote.widgets import SummernoteWidget # from django_summernote.widgets import SummernoteWidget
@ -270,7 +271,7 @@ class SourceAdvancedForm(forms.ModelForm):
class PersonForm(forms.ModelForm): class PersonForm(forms.ModelForm):
class Meta: class Meta:
model = Person 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 = { widgets = {
"first_name": forms.TextInput(attrs={'class': 'form-control'}), "first_name": forms.TextInput(attrs={'class': 'form-control'}),
"middle_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'}), "date_of_birth": forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}), "nationality": forms.Select(attrs={'class': 'form-control select2'}),
"address": forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), "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): class ApplicationForm(forms.ModelForm):
@ -790,11 +793,36 @@ class BulkInterviewTemplateForm(forms.ModelForm):
self.fields["applications"].queryset = Application.objects.filter( self.fields["applications"].queryset = Application.objects.filter(
job__slug=slug, stage="Interview" 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): def clean_working_days(self):
working_days = self.cleaned_data.get("working_days") working_days = self.cleaned_data.get("working_days")
return [int(day) for day in 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): class NoteForm(forms.ModelForm):
"""Form for creating and editing meeting comments""" """Form for creating and editing meeting comments"""
@ -2292,18 +2320,19 @@ class ApplicantSignupForm(forms.ModelForm):
class Meta: class Meta:
model = Person 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 = { widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}), 'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'middle_name': forms.TextInput(attrs={'class': 'form-control'}), 'middle_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}), 'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}), 'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}), 'phone': forms.TextInput(attrs={'class': 'form-control'}),
# 'gpa': forms.TextInput(attrs={'class': 'form-control'}), 'gpa': forms.TextInput(attrs={'class': 'custom-decimal-input'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}), "nationality": forms.Select(attrs={'class': 'form-control select2'}),
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'gender': forms.Select(attrs={'class': 'form-control'}), 'gender': forms.Select(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'national_id':forms.NumberInput(attrs={'min': 0, 'step': 1}),
} }
def clean(self): def clean(self):
@ -2878,4 +2907,17 @@ class ScheduledInterviewUpdateStatusForm(forms.Form):
) )
class Meta: class Meta:
model = ScheduledInterview model = ScheduledInterview
fields = ['status'] 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.models
import django.contrib.auth.validators import django.contrib.auth.validators
@ -8,6 +8,7 @@ import django.utils.timezone
import django_ckeditor_5.fields import django_ckeditor_5.fields
import django_countries.fields import django_countries.fields
import django_extensions.db.fields import django_extensions.db.fields
import encrypted_model_fields.fields
import recruitment.validators import recruitment.validators
from django.conf import settings from django.conf import settings
from django.db import migrations, models 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')), ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), ('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)), ('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')), ('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)), ('password', models.CharField(blank=True, max_length=20, null=True)),
('zoom_gateway_response', models.JSONField(blank=True, 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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last 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')), ('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')), ('email', encrypted_model_fields.fields.EncryptedEmailField(db_index=True, unique=True, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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 import F, Value, IntegerField, CharField,Q
from django.db.models.functions import Coalesce, Cast from django.db.models.functions import Coalesce, Cast
from django.db.models.fields.json import KeyTransform, KeyTextTransform from django.db.models.fields.json import KeyTransform, KeyTextTransform
from encrypted_model_fields.fields import EncryptedCharField,EncryptedEmailField,EncryptedTextField
class EmailContent(models.Model): class EmailContent(models.Model):
subject = models.CharField(max_length=255, verbose_name=_("Subject")) subject = models.CharField(max_length=255, verbose_name=_("Subject"))
@ -46,8 +47,8 @@ class CustomUser(AbstractUser):
user_type = models.CharField( user_type = models.CharField(
max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type") max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type")
) )
phone = models.CharField( phone = EncryptedCharField(
max_length=20, blank=True, null=True, verbose_name=_("Phone") blank=True, null=True, verbose_name=_("Phone")
) )
profile_image = models.ImageField( profile_image = models.ImageField(
null=True, null=True,
@ -59,7 +60,7 @@ class CustomUser(AbstractUser):
designation = models.CharField( designation = models.CharField(
max_length=100, blank=True, null=True, verbose_name=_("Designation") max_length=100, blank=True, null=True, verbose_name=_("Designation")
) )
email = models.EmailField( email = EncryptedEmailField(
unique=True, unique=True,
error_messages={ error_messages={
"unique": _("A user with this email already exists."), "unique": _("A user with this email already exists."),
@ -73,7 +74,7 @@ class CustomUser(AbstractUser):
@property @property
def get_unread_message_count(self): def get_unread_message_count(self):
message_list = ( 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 return message_list.count() or 0
@ -516,13 +517,13 @@ class Person(Base):
middle_name = models.CharField( middle_name = models.CharField(
max_length=255, blank=True, null=True, verbose_name=_("Middle Name") max_length=255, blank=True, null=True, verbose_name=_("Middle Name")
) )
email = models.EmailField( email = EncryptedEmailField(
unique=True, unique=True,
db_index=True, db_index=True,
verbose_name=_("Email"), verbose_name=_("Email"),
) )
phone = models.CharField( phone = EncryptedCharField(
max_length=20, blank=True, null=True, verbose_name=_("Phone") blank=True, null=True, verbose_name=_("Phone")
) )
date_of_birth = models.DateField( date_of_birth = models.DateField(
null=True, blank=True, verbose_name=_("Date of Birth") null=True, blank=True, verbose_name=_("Date of Birth")
@ -535,10 +536,13 @@ class Person(Base):
verbose_name=_("Gender"), verbose_name=_("Gender"),
) )
gpa = models.DecimalField( 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")) 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 # Optional linking to user account
user = models.OneToOneField( user = models.OneToOneField(
@ -1336,6 +1340,8 @@ class Interview(Base):
default=Status.WAITING, default=Status.WAITING,
db_index=True 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) # Remote-specific (nullable)
meeting_id = models.CharField( meeting_id = models.CharField(
@ -1784,7 +1790,7 @@ class FormSubmission(Base):
applicant_name = models.CharField( applicant_name = models.CharField(
max_length=200, blank=True, help_text="Name of the applicant" 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" db_index=True, blank=True, help_text="Email of the applicant"
) # Added index ) # Added index
@ -2086,8 +2092,8 @@ class HiringAgency(Base):
contact_person = models.CharField( contact_person = models.CharField(
max_length=150, blank=True, verbose_name=_("Contact Person") max_length=150, blank=True, verbose_name=_("Contact Person")
) )
email = models.EmailField(blank=True) email = EncryptedEmailField(blank=True)
phone = models.CharField(max_length=20, blank=True) phone = EncryptedCharField(max_length=20, blank=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency")) notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
country = CountryField(blank=True, null=True, blank_label=_("Select country")) country = CountryField(blank=True, null=True, blank_label=_("Select country"))
@ -2497,8 +2503,8 @@ class Participants(Base):
name = models.CharField( name = models.CharField(
max_length=255, verbose_name=_("Participant Name"), null=True, blank=True max_length=255, verbose_name=_("Participant Name"), null=True, blank=True
) )
email = models.EmailField(verbose_name=_("Email")) email =EncryptedEmailField(verbose_name=_("Email"))
phone = models.CharField( phone = EncryptedCharField(
max_length=12, verbose_name=_("Phone Number"), null=True, blank=True max_length=12, verbose_name=_("Phone Number"), null=True, blank=True
) )
designation = models.CharField( designation = models.CharField(

View File

@ -1,5 +1,5 @@
import logging import logging
import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.db import transaction from django.db import transaction
from django_q.models import Schedule 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}") 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) @receiver(post_save, sender=HiringAgency)
def hiring_agency_created(sender, instance, created, **kwargs): def hiring_agency_created(sender, instance, created, **kwargs):
if created: if created:

View File

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

View File

@ -9,6 +9,7 @@ from django.utils import timezone
from .models import ScheduledInterview from .models import ScheduledInterview
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.mail import send_mail from django.core.mail import send_mail
import random
# nlp = spacy.load("en_core_web_sm") # nlp = spacy.load("en_core_web_sm")
# def extract_text_from_pdf(pdf_path): # 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, StaffRequiredMixin,
StaffOrAgencyRequiredMixin, StaffOrAgencyRequiredMixin,
StaffOrCandidateRequiredMixin, StaffOrCandidateRequiredMixin,
superuser_required
) )
from .forms import ( from .forms import (
StaffUserCreationForm, StaffUserCreationForm,
@ -38,8 +39,10 @@ from .forms import (
StaffAssignmentForm, StaffAssignmentForm,
RemoteInterviewForm, RemoteInterviewForm,
OnsiteInterviewForm, OnsiteInterviewForm,
BulkInterviewTemplateForm BulkInterviewTemplateForm,
InterviewCancelForm
) )
from .utils import generate_random_password
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
@ -159,8 +162,12 @@ logger = logging.getLogger(__name__)
User = get_user_model() 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 model = Person
template_name = "people/person_list.html" template_name = "people/person_list.html"
context_object_name = "people_list" context_object_name = "people_list"
@ -197,7 +204,7 @@ class PersonListView(StaffRequiredMixin, ListView):
class PersonCreateView(CreateView): class PersonCreateView(CreateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin):
model = Person model = Person
template_name = "people/create_person.html" template_name = "people/create_person.html"
form_class = PersonForm form_class = PersonForm
@ -221,13 +228,13 @@ class PersonCreateView(CreateView):
class PersonDetailView(DetailView): class PersonDetailView(DetailView,LoginRequiredMixin,StaffRequiredMixin):
model = Person model = Person
template_name = "people/person_detail.html" template_name = "people/person_detail.html"
context_object_name = "person" context_object_name = "person"
class PersonUpdateView( UpdateView): class PersonUpdateView( UpdateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin):
model = Person model = Person
template_name = "people/update_person.html" template_name = "people/update_person.html"
form_class = PersonForm form_class = PersonForm
@ -239,7 +246,7 @@ class PersonUpdateView( UpdateView):
return redirect("agency_portal_persons_list") return redirect("agency_portal_persons_list")
return super().form_valid(form) return super().form_valid(form)
class PersonDeleteView(StaffRequiredMixin, DeleteView): class PersonDeleteView(StaffRequiredMixin, DeleteView,LoginRequiredMixin):
model = Person model = Person
template_name = "people/delete_person.html" template_name = "people/delete_person.html"
success_url = reverse_lazy("person_list") 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' from django.db.models import Avg, IntegerField, Value # Value is used for the default '0'
# These are essential for safely querying PostgreSQL JSONB fields # These are essential for safely querying PostgreSQL JSONB fields
from django.db.models.fields.json import KeyTransform, KeyTextTransform from django.db.models.fields.json import KeyTransform, KeyTextTransform
@staff_user_required @staff_user_required
@login_required
def job_detail(request, slug): def job_detail(request, slug):
"""View details of a specific job""" """View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
@ -711,7 +720,8 @@ def job_detail(request, slug):
# ) # )
# return response # return response
@login_required
@staff_user_required
def request_cvs_download(request, slug): def request_cvs_download(request, slug):
""" """
View to initiate the background task. 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.") 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 return redirect('job_detail', slug=slug) # Redirect back to the job detail page
@login_required
@staff_user_required
def download_ready_cvs(request, slug): def download_ready_cvs(request, slug):
""" """
View to serve the file once it is ready. View to serve the file once it is ready.
@ -985,12 +997,13 @@ def linkedin_callback(request):
# applicant views # applicant views
def applicant_job_detail(request, slug): # def applicant_job_detail(request, slug):
"""View job details for applicants""" # """View job details for applicants"""
job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE") # job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE")
return render(request, "jobs/applicant_job_detail.html", {"job": job}) # return render(request, "jobs/applicant_job_detail.html", {"job": job})
@login_required
@candidate_user_required
def application_success(request, slug): def application_success(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
return render(request, "jobs/application_success.html", {"job": job}) return render(request, "jobs/application_success.html", {"job": job})
@ -1012,6 +1025,8 @@ def form_builder(request, template_slug=None):
@csrf_exempt @csrf_exempt
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@login_required
@staff_user_required
def save_form_template(request): def save_form_template(request):
"""Save a new or existing form template""" """Save a new or existing form template"""
try: try:
@ -1073,6 +1088,8 @@ def save_form_template(request):
@require_http_methods(["GET"]) @require_http_methods(["GET"])
@login_required
@staff_user_required
def load_form_template(request, template_slug): def load_form_template(request, template_slug):
"""Load an existing form template""" """Load an existing form template"""
template = get_object_or_404(FormTemplate, slug=template_slug) 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): def application_submit_form(request, template_slug):
"""Display the form as a step-by-step wizard""" """Display the form as a step-by-step wizard"""
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -1282,7 +1300,10 @@ def application_submit_form(request, template_slug):
@csrf_exempt @csrf_exempt
@require_POST @require_POST
@login_required
@candidate_user_required
def application_submit(request, template_slug): def application_submit(request, template_slug):
import re
"""Handle form submission""" """Handle form submission"""
if not request.user.is_authenticated :# or request.user.user_type != "candidate": if not request.user.is_authenticated :# or request.user.user_type != "candidate":
return JsonResponse({"success": False, "message": "Unauthorized access."}) 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") # phone = submission.responses.get(field__label="Phone Number")
# address = submission.responses.get(field__label="Address") # address = submission.responses.get(field__label="Address")
gpa = submission.responses.get(field__label="GPA") 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") resume = submission.responses.get(field__label="Resume Upload")
@ -1450,6 +1494,7 @@ def form_template_all_submissions(request, template_id):
@login_required @login_required
@staff_user_required
def form_submission_details(request, template_id, slug): def form_submission_details(request, template_id, slug):
"""Display detailed view of a specific form submission""" """Display detailed view of a specific form submission"""
# Get the form template and verify ownership # 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): def _handle_get_request(request, slug, job):
""" """
Handles GET requests, setting up forms and restoring candidate selections 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): def _handle_preview_submission(request, slug, job):
""" """
Handles the initial POST request (Preview Schedule). 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): def _handle_confirm_schedule(request, slug, job):
""" """
Handles the final POST request (Confirm Schedule). Handles the final POST request (Confirm Schedule).
@ -1754,6 +1804,8 @@ def _handle_confirm_schedule(request, slug, job):
return redirect("schedule_interviews", slug=slug) return redirect("schedule_interviews", slug=slug)
@login_required
@staff_user_required
def schedule_interviews_view(request, slug): def schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST": if request.method == "POST":
@ -1766,6 +1818,8 @@ def schedule_interviews_view(request, slug):
# return redirect("applications_interview_view", slug=slug) # return redirect("applications_interview_view", slug=slug)
@login_required
@staff_user_required
def confirm_schedule_interviews_view(request, slug): def confirm_schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST": if request.method == "POST":
@ -1773,6 +1827,7 @@ def confirm_schedule_interviews_view(request, slug):
return _handle_confirm_schedule(request, slug, job) return _handle_confirm_schedule(request, slug, job)
@login_required
@staff_user_required @staff_user_required
def applications_screening_view(request, slug): 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) return render(request, "recruitment/applications_screening_view.html", context)
@login_required
@staff_user_required @staff_user_required
def applications_exam_view(request, slug): 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) return render(request, "recruitment/applications_exam_view.html", context)
@login_required
@staff_user_required @staff_user_required
def update_application_exam_status(request, slug): def update_application_exam_status(request, slug):
application = get_object_or_404(Application, slug=slug) application = get_object_or_404(Application, slug=slug)
@ -1881,7 +1938,7 @@ def update_application_exam_status(request, slug):
{"application": application, "form": form}, {"application": application, "form": form},
) )
@login_required
@staff_user_required @staff_user_required
def bulk_update_application_exam_status(request, slug): def bulk_update_application_exam_status(request, slug):
job = get_object_or_404(JobPosting, slug=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) return redirect("applications_exam_view", slug=job.slug)
@login_required
@staff_user_required
def application_criteria_view_htmx(request, pk): def application_criteria_view_htmx(request, pk):
application = get_object_or_404(Application, pk=pk) application = get_object_or_404(Application, pk=pk)
return render( return render(
request, "includes/application_modal_body.html", {"application": application} request, "includes/application_modal_body.html", {"application": application}
) )
@login_required
@staff_user_required @staff_user_required
def application_set_exam_date(request, slug): def application_set_exam_date(request, slug):
application = get_object_or_404(Application, slug=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) return redirect("applications_screening_view", slug=application.job.slug)
@login_required
@staff_user_required @staff_user_required
def application_update_status(request, slug): def application_update_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
@ -1998,6 +2057,7 @@ def application_update_status(request, slug):
return response return response
@login_required
@staff_user_required @staff_user_required
def applications_interview_view(request, slug): def applications_interview_view(request, slug):
job = get_object_or_404(JobPosting, slug=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) return render(request, "recruitment/applications_interview_view.html", context)
@login_required
@staff_user_required @staff_user_required
def applications_document_review_view(request, slug): 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) return render(request, "recruitment/applications_document_review_view.html", context)
@login_required
@require_POST @require_POST
@staff_user_required @staff_user_required
def reschedule_meeting_for_application(request, slug): def reschedule_meeting_for_application(request, slug):
@ -2150,62 +2211,62 @@ def reschedule_meeting_for_application(request, slug):
# @staff_user_required # @staff_user_required
# def interview_calendar_view(request, slug): # 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 # # Get all scheduled interviews for this job
scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( # scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
"applicaton", "zoom_meeting" # "applicaton", "zoom_meeting"
) # )
# Convert interviews to calendar events # # Convert interviews to calendar events
events = [] # events = []
for interview in scheduled_interviews: # for interview in scheduled_interviews:
# Create start datetime # # Create start datetime
start_datetime = datetime.combine( # start_datetime = datetime.combine(
interview.interview_date, interview.interview_time # interview.interview_date, interview.interview_time
) # )
# Calculate end datetime based on interview duration # # Calculate end datetime based on interview duration
duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 # duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60
end_datetime = start_datetime + timedelta(minutes=duration) # end_datetime = start_datetime + timedelta(minutes=duration)
# Determine event color based on status # # Determine event color based on status
color = "#00636e" # Default color # color = "#00636e" # Default color
if interview.status == "confirmed": # if interview.status == "confirmed":
color = "#00a86b" # Green for confirmed # color = "#00a86b" # Green for confirmed
elif interview.status == "cancelled": # elif interview.status == "cancelled":
color = "#e74c3c" # Red for cancelled # color = "#e74c3c" # Red for cancelled
elif interview.status == "completed": # elif interview.status == "completed":
color = "#95a5a6" # Gray for completed # color = "#95a5a6" # Gray for completed
events.append( # events.append(
{ # {
"title": f"Interview: {interview.candidate.name}", # "title": f"Interview: {interview.candidate.name}",
"start": start_datetime.isoformat(), # "start": start_datetime.isoformat(),
"end": end_datetime.isoformat(), # "end": end_datetime.isoformat(),
"url": f"{request.path}interview/{interview.id}/", # "url": f"{request.path}interview/{interview.id}/",
"color": color, # "color": color,
"extendedProps": { # "extendedProps": {
"candidate": interview.candidate.name, # "candidate": interview.candidate.name,
"email": interview.candidate.email, # "email": interview.candidate.email,
"status": interview.status, # "status": interview.status,
"meeting_id": interview.zoom_meeting.meeting_id # "meeting_id": interview.zoom_meeting.meeting_id
if interview.zoom_meeting # if interview.zoom_meeting
else None, # else None,
"join_url": interview.zoom_meeting.join_url # "join_url": interview.zoom_meeting.join_url
if interview.zoom_meeting # if interview.zoom_meeting
else None, # else None,
}, # },
} # }
) # )
context = { # context = {
"job": job, # "job": job,
"events": events, # "events": events,
"calendar_color": "#00636e", # "calendar_color": "#00636e",
} # }
return render(request, "recruitment/interview_calendar.html", context) # return render(request, "recruitment/interview_calendar.html", context)
# @staff_user_required # @staff_user_required
@ -2890,7 +2951,7 @@ def reschedule_meeting_for_application(request, slug):
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@login_required
def user_profile_image_update(request, pk): def user_profile_image_update(request, pk):
user = get_object_or_404(User, pk=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) return render(request, "user/profile.html", context)
@login_required
def user_detail(request, pk): def user_detail(request, pk):
user = get_object_or_404(User, pk=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) return render(request, "user/profile.html", context)
@login_required
@staff_user_required
def easy_logs(request): def easy_logs(request):
""" """
Function-based view to display Django Easy Audit logs with tab switching and pagination. 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 return user.is_superuser
@staff_user_required @login_required
@superuser_required
def create_staff_user(request): def create_staff_user(request):
if request.method == "POST": if request.method == "POST":
form = StaffUserCreationForm(request.POST) form = StaffUserCreationForm(request.POST)
@ -3000,13 +3064,15 @@ def create_staff_user(request):
return render(request, "user/create_staff.html", {"form": form}) return render(request, "user/create_staff.html", {"form": form})
@staff_user_required @login_required
@superuser_required
def admin_settings(request): def admin_settings(request):
staffs = User.objects.filter(user_type="staff",is_superuser=False) staffs = User.objects.filter(user_type="staff",is_superuser=False)
form = ToggleAccountForm() form = ToggleAccountForm()
context = {"staffs": staffs, "form": form} context = {"staffs": staffs, "form": form}
return render(request, "user/admin_settings.html", context) return render(request, "user/admin_settings.html", context)
@login_required
@staff_user_required @staff_user_required
def staff_assignment_view(request, slug): def staff_assignment_view(request, slug):
""" """
@ -3040,8 +3106,8 @@ def staff_assignment_view(request, slug):
from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.forms import SetPasswordForm
@login_required
@staff_user_required @superuser_required
def set_staff_password(request, pk): def set_staff_password(request, pk):
user = get_object_or_404(User, pk=pk) user = get_object_or_404(User, pk=pk)
print(request.POST) print(request.POST)
@ -3062,8 +3128,8 @@ def set_staff_password(request, pk):
request, "user/staff_password_create.html", {"form": form, "user": user} request, "user/staff_password_create.html", {"form": form, "user": user}
) )
@login_required
@staff_user_required @superuser_required
def account_toggle_status(request, pk): def account_toggle_status(request, pk):
user = get_object_or_404(User, pk=pk) user = get_object_or_404(User, pk=pk)
if request.method == "POST": if request.method == "POST":
@ -3273,6 +3339,7 @@ def zoom_webhook_view(request):
# Hiring Agency CRUD Views # Hiring Agency CRUD Views
@login_required
@staff_user_required @staff_user_required
def agency_list(request): def agency_list(request):
"""List all hiring agencies with search and pagination""" """List all hiring agencies with search and pagination"""
@ -3303,6 +3370,8 @@ def agency_list(request):
return render(request, "recruitment/agency_list.html", context) return render(request, "recruitment/agency_list.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_create(request): def agency_create(request):
"""Create a new hiring agency""" """Create a new hiring agency"""
@ -3325,6 +3394,31 @@ def agency_create(request):
return render(request, "recruitment/agency_form.html", context) 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 @staff_user_required
def agency_detail(request, slug): def agency_detail(request, slug):
"""View details of a specific hiring agency""" """View details of a specific hiring agency"""
@ -3360,7 +3454,7 @@ def agency_detail(request, slug):
} }
return render(request, "recruitment/agency_detail.html", context) return render(request, "recruitment/agency_detail.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_update(request, slug): def agency_update(request, slug):
"""Update an existing hiring agency""" """Update an existing hiring agency"""
@ -3385,7 +3479,7 @@ def agency_update(request, slug):
} }
return render(request, "recruitment/agency_form.html", context) return render(request, "recruitment/agency_form.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_delete(request, slug): def agency_delete(request, slug):
"""Delete a hiring agency""" """Delete a hiring agency"""
@ -3716,7 +3810,7 @@ def agency_delete(request, slug):
# } # }
# return render(request, 'recruitment/agency_candidates.html', context) # return render(request, 'recruitment/agency_candidates.html', context)
@login_required
@staff_user_required @staff_user_required
def agency_applications(request, slug): def agency_applications(request, slug):
"""View all applications from a specific agency""" """View all applications from a specific agency"""
@ -3748,6 +3842,7 @@ def agency_applications(request, slug):
# Agency Portal Management Views # Agency Portal Management Views
@login_required
@staff_user_required @staff_user_required
def agency_assignment_list(request): def agency_assignment_list(request):
"""List all agency job assignments""" """List all agency job assignments"""
@ -3782,6 +3877,7 @@ def agency_assignment_list(request):
return render(request, "recruitment/agency_assignment_list.html", context) return render(request, "recruitment/agency_assignment_list.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_assignment_create(request, slug=None): def agency_assignment_create(request, slug=None):
"""Create a new agency job assignment""" """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) return render(request, "recruitment/agency_assignment_form.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_assignment_detail(request, slug): def agency_assignment_detail(request, slug):
"""View details of a specific agency assignment""" """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) return render(request, "recruitment/agency_assignment_detail.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_assignment_update(request, slug): def agency_assignment_update(request, slug):
"""Update an existing agency assignment""" """Update an existing agency assignment"""
@ -3881,7 +3978,7 @@ def agency_assignment_update(request, slug):
} }
return render(request, "recruitment/agency_assignment_form.html", context) return render(request, "recruitment/agency_assignment_form.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_access_link_create(request): def agency_access_link_create(request):
"""Create access link for agency assignment""" """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) return render(request, "recruitment/agency_access_link_form.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_access_link_detail(request, slug): def agency_access_link_detail(request, slug):
"""View details of an access link""" """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) return render(request, "recruitment/agency_access_link_detail.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_assignment_extend_deadline(request, slug): def agency_assignment_extend_deadline(request, slug):
"""Extend deadline for an agency assignment""" """Extend deadline for an agency assignment"""
@ -4124,6 +4222,7 @@ def applicant_portal_dashboard(request):
@login_required @login_required
@candidate_user_required
def applicant_application_detail(request, slug): def applicant_application_detail(request, slug):
"""View detailed information about a specific application""" """View detailed information about a specific application"""
if not request.user.is_authenticated: 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) return render(request, "recruitment/applicant_application_detail.html", context)
@login_required
@agency_user_required @agency_user_required
def agency_portal_persons_list(request): def agency_portal_persons_list(request):
"""Agency portal page showing all persons who come through this agency""" """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) return render(request, "recruitment/agency_portal_persons_list.html", context)
@login_required
@agency_user_required @agency_user_required
def agency_portal_dashboard(request): def agency_portal_dashboard(request):
"""Agency portal dashboard showing all assignments for the agency""" """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) return render(request, "recruitment/agency_portal_dashboard.html", context)
@login_required
@agency_user_required @agency_user_required
def agency_portal_submit_application_page(request, slug): def agency_portal_submit_application_page(request, slug):
"""Dedicated page for submitting a application """ """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) return render(request, "recruitment/agency_portal_submit_application.html", context)
@login_required
@agency_user_required @agency_user_required
def agency_portal_submit_application(request): def agency_portal_submit_application(request):
"""Handle candidate submission via AJAX (for embedded form)""" """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) return render(request, "recruitment/agency_portal_submit_application.html", context)
@login_required
@staff_or_agency_required
def agency_portal_assignment_detail(request, slug): def agency_portal_assignment_detail(request, slug):
"""View details of a specific assignment - routes to admin or agency template""" """View details of a specific assignment - routes to admin or agency template"""
assignment = get_object_or_404( assignment = get_object_or_404(
@ -4439,6 +4543,7 @@ def agency_portal_assignment_detail(request, slug):
return redirect("portal_login") return redirect("portal_login")
@login_required
@agency_user_required @agency_user_required
def agency_assignment_detail_agency(request, slug, assignment_id): def agency_assignment_detail_agency(request, slug, assignment_id):
"""Handle agency portal assignment detail view""" """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) return render(request, "recruitment/agency_portal_assignment_detail.html", context)
@login_required
@staff_user_required @staff_user_required
def agency_assignment_detail_admin(request, slug): def agency_assignment_detail_admin(request, slug):
"""Handle admin assignment detail view""" """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 #will check the changes application to appliaction in this function
@login_required
@agency_user_required @agency_user_required
def agency_portal_edit_application(request, candidate_id): def agency_portal_edit_application(request, candidate_id):
"""Edit a candidate for agency portal""" """Edit a candidate for agency portal"""
@ -4591,6 +4698,7 @@ def agency_portal_edit_application(request, candidate_id):
return redirect("agency_portal_dashboard") return redirect("agency_portal_dashboard")
@login_required
@agency_user_required @agency_user_required
def agency_portal_delete_application(request, candidate_id): def agency_portal_delete_application(request, candidate_id):
"""Delete a candidate for agency portal""" """Delete a candidate for agency portal"""
@ -4629,7 +4737,7 @@ def agency_portal_delete_application(request, candidate_id):
# Message Views # Message Views
@login_required
def message_list(request): def message_list(request):
"""List all messages for the current user""" """List all messages for the current user"""
# Get filter parameters # Get filter parameters
@ -4721,10 +4829,14 @@ def message_create(request):
# Send email if message_type is 'email' and recipient has email # Send email if message_type is 'email' and recipient has email
if message.recipient and message.recipient.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: try:
email_result = async_task('recruitment.tasks._task_send_individual_email', email_result = async_task('recruitment.tasks._task_send_individual_email',
subject=message.subject, subject=message.subject,
body_message=message.content, body_message=message,
recipient=message.recipient.email, recipient=message.recipient.email,
attachments=None, attachments=None,
sender=False, sender=False,
@ -5262,6 +5374,7 @@ def portal_logout(request):
# Interview Creation Views # Interview Creation Views
@login_required
@staff_user_required @staff_user_required
def interview_create_type_selection(request, application_slug): def interview_create_type_selection(request, application_slug):
"""Show interview type selection page for a application""" """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) return render(request, 'interviews/interview_create_type_selection.html', context)
@login_required
@staff_user_required @staff_user_required
def interview_create_remote(request, application_slug): def interview_create_remote(request, application_slug):
"""Create remote interview for a candidate""" """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) return render(request, 'interviews/interview_create_remote.html', context)
@login_required
@staff_user_required @staff_user_required
def interview_create_onsite(request, application_slug): def interview_create_onsite(request, application_slug):
"""Create onsite interview for a candidate""" """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) return render(request, 'interviews/interview_create_onsite.html', context)
@login_required
@staff_user_required
def get_interview_list(request, job_slug): def get_interview_list(request, job_slug):
from .forms import ScheduledInterviewUpdateStatusForm from .forms import ScheduledInterviewUpdateStatusForm
application = Application.objects.get(slug=job_slug) application = Application.objects.get(slug=job_slug)
@ -5351,6 +5468,9 @@ def get_interview_list(request, job_slug):
interview_status_form = ScheduledInterviewUpdateStatusForm() interview_status_form = ScheduledInterviewUpdateStatusForm()
return render(request, 'interviews/partials/interview_list.html', {'interviews': interviews, 'application': application,'interview_status_form':interview_status_form}) 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 @require_POST
def update_interview_status(request,slug): def update_interview_status(request,slug):
from .forms import ScheduledInterviewUpdateStatusForm from .forms import ScheduledInterviewUpdateStatusForm
@ -5364,22 +5484,59 @@ def update_interview_status(request,slug):
messages.success(request, "Interview status updated successfully.") messages.success(request, "Interview status updated successfully.")
return redirect('interview_detail', slug=slug) return redirect('interview_detail', slug=slug)
@require_POST # @require_POST
def cancel_interview_for_application(request,slug): # def cancel_interview_for_application(request,slug):
scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug) # scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
if request.method == 'POST': # if request.method == 'POST':
if scheduled_interview.interview_type == 'REMOTE': # if scheduled_interview.interview_type == 'REMOTE':
result = delete_zoom_meeting(scheduled_interview.interview.meeting_id) # result = delete_zoom_meeting(scheduled_interview.interview.meeting_id)
if result["status"] != "success": # if result["status"] != "success":
messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}") # messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}")
return redirect('interview_detail', slug=slug) # return redirect('interview_detail', slug=slug)
scheduled_interview.delete() # scheduled_interview.delete()
messages.success(request, "Interview cancelled successfully.") # messages.success(request, "Interview cancelled successfully.")
return redirect('interview_list') # 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)
@login_required @login_required
@staff_user_required
def agency_access_link_deactivate(request, slug): def agency_access_link_deactivate(request, slug):
"""Deactivate an agency access link""" """Deactivate an agency access link"""
access_link = get_object_or_404( access_link = get_object_or_404(
@ -5417,6 +5574,7 @@ def agency_access_link_deactivate(request, slug):
@login_required @login_required
@staff_user_required
def agency_access_link_reactivate(request, slug): def agency_access_link_reactivate(request, slug):
"""Reactivate an agency access link""" """Reactivate an agency access link"""
access_link = get_object_or_404( 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) return render(request, "recruitment/agency_access_link_confirm.html", context)
@agency_user_required @agency_user_required
def api_application_detail(request, candidate_id): def api_application_detail(request, candidate_id):
"""API endpoint to get candidate details for agency portal""" """API endpoint to get candidate details for agency portal"""
@ -5490,7 +5649,7 @@ def api_application_detail(request, candidate_id):
except Exception as e: except Exception as e:
return JsonResponse({"success": False, "error": str(e)}) return JsonResponse({"success": False, "error": str(e)})
@login_required
@staff_user_required @staff_user_required
def compose_application_email(request, job_slug): def compose_application_email(request, job_slug):
"""Compose email to participants about a candidate""" """Compose email to participants about a candidate"""
@ -5625,7 +5784,7 @@ def compose_application_email(request, job_slug):
else: else:
# GET request - show the form # GET request - show the form
form = CandidateEmailForm(job, candidates) form = CandidateEmailForm(job, candidates,request)
return render( return render(
request, request,
@ -5637,6 +5796,7 @@ def compose_application_email(request, job_slug):
# Source CRUD Views # Source CRUD Views
@login_required
@staff_user_required @staff_user_required
def source_list(request): def source_list(request):
"""List all sources with search and pagination""" """List all sources with search and pagination"""
@ -5666,6 +5826,7 @@ def source_list(request):
return render(request, "recruitment/source_list.html", context) return render(request, "recruitment/source_list.html", context)
@login_required
@staff_user_required @staff_user_required
def source_create(request): def source_create(request):
"""Create a new source""" """Create a new source"""
@ -5688,6 +5849,7 @@ def source_create(request):
return render(request, "recruitment/source_form.html", context) return render(request, "recruitment/source_form.html", context)
@login_required
@staff_user_required @staff_user_required
def source_detail(request, slug): def source_detail(request, slug):
"""View details of a specific source""" """View details of a specific source"""
@ -5715,6 +5877,7 @@ def source_detail(request, slug):
return render(request, "recruitment/source_detail.html", context) return render(request, "recruitment/source_detail.html", context)
@login_required
@staff_user_required @staff_user_required
def source_update(request, slug): def source_update(request, slug):
"""Update an existing source""" """Update an existing source"""
@ -5740,6 +5903,7 @@ def source_update(request, slug):
return render(request, "recruitment/source_form.html", context) return render(request, "recruitment/source_form.html", context)
@login_required
@staff_user_required @staff_user_required
def source_delete(request, slug): def source_delete(request, slug):
"""Delete a source""" """Delete a source"""
@ -5761,6 +5925,7 @@ def source_delete(request, slug):
@login_required @login_required
@staff_user_required
def source_generate_keys(request, slug): def source_generate_keys(request, slug):
"""Generate new API keys for a source""" """Generate new API keys for a source"""
source = get_object_or_404(Source, slug=slug) source = get_object_or_404(Source, slug=slug)
@ -5787,6 +5952,7 @@ def source_generate_keys(request, slug):
@login_required @login_required
@staff_user_required
def source_toggle_status(request, slug): def source_toggle_status(request, slug):
"""Toggle active status of a source""" """Toggle active status of a source"""
source = get_object_or_404(Source, slug=slug) source = get_object_or_404(Source, slug=slug)
@ -5868,6 +6034,7 @@ def application_signup(request, slug):
# Interview Views # Interview Views
@login_required
@staff_user_required @staff_user_required
def interview_list(request): def interview_list(request):
"""List all interviews with filtering and pagination""" """List all interviews with filtering and pagination"""
@ -5906,20 +6073,23 @@ def interview_list(request):
} }
return render(request, 'interviews/interview_list.html', context) return render(request, 'interviews/interview_list.html', context)
@login_required
@staff_user_required @staff_user_required
def interview_detail(request, slug): def interview_detail(request, slug):
"""View details of a specific interview""" """View details of a specific interview"""
from .forms import ScheduledInterviewUpdateStatusForm from .forms import ScheduledInterviewUpdateStatusForm
interview = get_object_or_404(ScheduledInterview, slug=slug) interview = get_object_or_404(ScheduledInterview, slug=slug)
reschedule_form = ScheduledInterviewForm() reschedule_form = ScheduledInterviewForm()
reschedule_form.initial['topic'] = interview.interview.topic reschedule_form.initial['topic'] = interview.interview.topic
meeting=interview.interview
context = { context = {
'interview': interview, 'interview': interview,
'reschedule_form':reschedule_form, '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) return render(request, 'interviews/interview_detail.html', context)
@ -6603,6 +6773,8 @@ def interview_detail(request, slug):
# return redirect('meeting_details', slug=slug) # return redirect('meeting_details', slug=slug)
@login_required
@staff_user_required
def application_add_note(request, slug): def application_add_note(request, slug):
from .models import Note from .models import Note
from .forms import NoteForm from .forms import NoteForm
@ -6631,6 +6803,8 @@ def application_add_note(request, slug):
notes = Note.objects.filter(application=application).order_by('-created_at') 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}) 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): def interview_add_note(request, slug):
from .models import Note from .models import Note
from .forms import NoteForm from .forms import NoteForm
@ -6656,3 +6830,25 @@ def interview_add_note(request, slug):
form.fields['author'].widget = HiddenInput() form.fields['author'].widget = HiddenInput()
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()}) 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' slug_url_kwarg = 'slug'
@login_required
@staff_user_required
def retry_scoring_view(request,slug): def retry_scoring_view(request,slug):
from django_q.tasks import async_task from django_q.tasks import async_task
@ -302,6 +304,10 @@ def application_update_stage(request, slug):
application.save(update_fields=['stage']) application.save(update_fields=['stage'])
messages.success(request,_("application Stage Updated")) messages.success(request,_("application Stage Updated"))
return redirect("application_detail",slug=application.slug) return redirect("application_detail",slug=application.slug)
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView): class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.TrainingMaterial model = models.TrainingMaterial
@ -755,7 +761,7 @@ STAGE_CONFIG = {
'headers': ['Name', 'Email', 'Phone', 'Application Date', 'Offer Status', 'Offer Date', 'Match Score', 'Years Experience', 'Professional Category'] 'headers': ['Name', 'Email', 'Phone', 'Application Date', 'Offer Status', 'Offer Date', 'Match Score', 'Years Experience', 'Professional Category']
}, },
'hired': { 'hired': {
'filter': {'offer_status': 'Accepted'}, 'filter': {'stage': 'Hired'},
'fields': ['name', 'email', 'phone', 'created_at', 'offer_date', 'ai_score', 'years_experience', 'professional_category', 'join_date'], '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'] '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 import string
from .models import Source, IntegrationLog from .models import Source, IntegrationLog
from .forms import SourceForm, generate_api_key, generate_api_secret from .forms import SourceForm, generate_api_key, generate_api_secret
from .decorators import login_required, staff_user_required
class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView): class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
"""List all sources""" """List all sources"""
@ -182,6 +183,8 @@ class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
messages.success(request, f'Source "{self.object.name}" deleted successfully!') messages.success(request, f'Source "{self.object.name}" deleted successfully!')
return super().delete(request, *args, **kwargs) return super().delete(request, *args, **kwargs)
@login_required
@staff_user_required
def generate_api_keys_view(request, pk): def generate_api_keys_view(request, pk):
"""Generate new API keys for a specific source""" """Generate new API keys for a specific source"""
if not request.user.is_staff: 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) return JsonResponse({'error': 'Invalid request method'}, status=405)
@login_required
@staff_user_required
def toggle_source_status_view(request, pk): def toggle_source_status_view(request, pk):
"""Toggle the active status of a source""" """Toggle the active status of a source"""
if not request.user.is_staff: if not request.user.is_staff:
@ -267,7 +272,7 @@ def toggle_source_status_view(request, pk):
# 'is_active': source.is_active, # 'is_active': source.is_active,
# 'message': f'Source "{source.name}" {status_text} successfully' # 'message': f'Source "{source.name}" {status_text} successfully'
# }) # })
@login_required
def copy_to_clipboard_view(request): def copy_to_clipboard_view(request):
"""HTMX endpoint to copy text to clipboard""" """HTMX endpoint to copy text to clipboard"""
if request.method == 'POST': 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 amqp==5.3.1
annotated-types==0.7.0 annotated-types==0.7.0
anthropic==0.63.0
anyio==4.11.0
appdirs==1.4.4 appdirs==1.4.4
arrow==1.3.0 arrow==1.3.0
asgiref==3.9.2 asgiref==3.10.0
asteval==1.0.6 asteval==1.0.6
astunparse==1.6.3 astunparse==1.6.3
attrs==25.3.0 attrs==25.3.0
@ -13,8 +11,8 @@ bleach==6.2.0
blessed==1.22.0 blessed==1.22.0
blinker==1.9.0 blinker==1.9.0
blis==1.3.0 blis==1.3.0
boto3==1.40.37 boto3==1.40.45
botocore==1.40.37 botocore==1.40.45
bw-migrations==0.2 bw-migrations==0.2
bw2data==4.5 bw2data==4.5
bw2parameters==1.1.0 bw2parameters==1.1.0
@ -22,7 +20,8 @@ bw_processing==1.0
cached-property==2.0.1 cached-property==2.0.1
catalogue==2.0.10 catalogue==2.0.10
celery==5.5.3 celery==5.5.3
certifi==2025.8.3 certifi==2025.10.5
cffi==2.0.0
channels==4.3.1 channels==4.3.1
chardet==5.2.0 chardet==5.2.0
charset-normalizer==3.4.3 charset-normalizer==3.4.3
@ -35,50 +34,49 @@ confection==0.1.5
constructive_geometries==1.0 constructive_geometries==1.0
country_converter==1.3.1 country_converter==1.3.1
crispy-bootstrap5==2025.6 crispy-bootstrap5==2025.6
cryptography==46.0.2
cymem==2.0.11 cymem==2.0.11
dataflows-tabulator==1.54.3 dataflows-tabulator==1.54.3
datapackage==1.15.4 datapackage==1.15.4
datastar-py==0.6.5 datastar-py==0.6.5
deepdiff==7.0.1 deepdiff==7.0.1
Deprecated==1.2.18 Deprecated==1.2.18
distro==1.9.0 Django==5.2.7
Django==5.2.6 django-allauth==65.12.1
django-allauth==65.11.2
django-ckeditor-5==0.2.18 django-ckeditor-5==0.2.18
django-cors-headers==4.9.0 django-cors-headers==4.9.0
django-countries==7.6.1 django-countries==7.6.1
django-crispy-forms==2.4 django-crispy-forms==2.4
django-easy-audit==1.3.7 django-easy-audit==1.3.7
django-encrypted-model-fields==0.6.5
django-extensions==4.1 django-extensions==4.1
django-filter==25.1 django-filter==25.1
django-js-asset==3.1.2
django-picklefield==3.3 django-picklefield==3.3
django-q2==1.8.0 django-q2==1.8.0
django-summernote==0.8.20.0
django-template-partials==25.2 django-template-partials==25.2
django-unfold==0.66.0 django-unfold==0.67.0
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
django_celery_results==2.6.0 django_celery_results==2.6.0
djangorestframework==3.16.1 djangorestframework==3.16.1
docopt==0.6.2 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 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 et_xmlfile==2.0.0
Faker==37.8.0 Faker==37.8.0
filelock==3.19.1
flexcache==0.3 flexcache==0.3
flexparser==0.4 flexparser==0.4
fsspec==2025.9.0 fsspec==2025.9.0
gpt-po-translator==1.3.2
greenlet==3.2.4 greenlet==3.2.4
h11==0.16.0 hf-xet==1.1.10
httpcore==1.0.9 huggingface-hub==0.35.3
httpx==0.28.1
idna==3.10 idna==3.10
ijson==3.4.0 ijson==3.4.0
iniconfig==2.1.0
isodate==0.7.2 isodate==0.7.2
isort==5.13.2
Jinja2==3.1.6 Jinja2==3.1.6
jiter==0.11.1
jmespath==1.0.1 jmespath==1.0.1
joblib==1.5.2
jsonlines==4.0.0 jsonlines==4.0.0
jsonpointer==3.0.0 jsonpointer==3.0.0
jsonschema==4.25.1 jsonschema==4.25.1
@ -87,37 +85,52 @@ kombu==5.5.4
langcodes==3.5.0 langcodes==3.5.0
language_data==1.3.0 language_data==1.3.0
linear-tsv==1.1.0 linear-tsv==1.1.0
llvmlite==0.45.0 llvmlite==0.45.1
loguru==0.7.3 loguru==0.7.3
lxml==6.0.2 lxml==6.0.2
marisa-trie==1.3.1 marisa-trie==1.3.1
markdown-it-py==4.0.0 markdown-it-py==4.0.0
MarkupSafe==3.0.2 MarkupSafe==3.0.3
matrix_utils==0.6.2 matrix_utils==0.6.2
mdurl==0.1.2 mdurl==0.1.2
morefs==0.2.2 morefs==0.2.2
mpmath==1.3.0
mrio-common-metadata==0.2.1 mrio-common-metadata==0.2.1
murmurhash==1.0.13 murmurhash==1.0.13
numba==0.62.0 networkx==3.5
numba==0.62.1
numpy==2.3.3 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 openpyxl==3.1.5
ordered-set==4.1.0 ordered-set==4.1.0
packaging==25.0 packaging==25.0
pandas==2.3.2 pandas==2.3.3
pdfminer.six==20250506
pdfplumber==0.11.7
peewee==3.18.2 peewee==3.18.2
pillow==11.3.0 pillow==11.3.0
Pint==0.25 Pint==0.25
platformdirs==4.4.0 platformdirs==4.4.0
pluggy==1.6.0
polib==1.2.0
preshed==3.0.10 preshed==3.0.10
prettytable==3.16.0 prettytable==3.16.0
prompt_toolkit==3.0.52 prompt_toolkit==3.0.52
psycopg2-binary==2.9.11 psycopg==3.2.11
pycountry==24.6.1 pycparser==2.23
pydantic==2.11.9 pydantic==2.11.10
pydantic-settings==2.10.1 pydantic-settings==2.11.0
pydantic_core==2.33.2 pydantic_core==2.33.2
pyecospold==4.0.0 pyecospold==4.0.0
Pygments==2.19.2 Pygments==2.19.2
@ -125,35 +138,36 @@ PyJWT==2.10.1
PyMuPDF==1.26.4 PyMuPDF==1.26.4
pyparsing==3.2.5 pyparsing==3.2.5
PyPDF2==3.0.1 PyPDF2==3.0.1
pypdfium2==4.30.0
PyPrind==2.11.3 PyPrind==2.11.3
pytest==8.3.4 pytesseract==0.3.13
pytest-django==4.11.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-docx==1.2.0 python-docx==1.2.0
python-dotenv==1.0.1 python-dotenv==1.1.1
python-json-logger==3.3.0 python-json-logger==3.3.0
pytz==2025.2 pytz==2025.2
pyxlsb==1.0.10 pyxlsb==1.0.10
PyYAML==6.0.2 PyYAML==6.0.3
randonneur==0.6.2 randonneur==0.6.2
randonneur_data==0.6 randonneur_data==0.6.1
RapidFuzz==3.14.1 RapidFuzz==3.14.1
rdflib==7.2.1 rdflib==7.2.1
redis==3.5.3 redis==3.5.3
referencing==0.36.2 referencing==0.36.2
requests==2.32.3 regex==2025.9.18
responses==0.25.8 requests==2.32.5
rfc3986==2.0.0 rfc3986==2.0.0
rich==14.1.0 rich==14.1.0
rpds-py==0.27.1 rpds-py==0.27.1
s3transfer==0.14.0 s3transfer==0.14.0
safetensors==0.6.2
scikit-learn==1.7.2
scipy==1.16.2 scipy==1.16.2
sentence-transformers==5.1.1
setuptools==80.9.0 setuptools==80.9.0
setuptools-scm==8.1.0
shellingham==1.5.4 shellingham==1.5.4
six==1.17.0 six==1.17.0
smart_open==7.3.1 smart_open==7.3.1
sniffio==1.3.1
snowflake-id==1.0.2 snowflake-id==1.0.2
spacy==3.8.7 spacy==3.8.7
spacy-legacy==3.0.12 spacy-legacy==3.0.12
@ -165,15 +179,19 @@ sqlparse==0.5.3
srsly==2.5.1 srsly==2.5.1
stats_arrays==0.7 stats_arrays==0.7
structlog==25.4.0 structlog==25.4.0
sympy==1.14.0
tableschema==1.21.0 tableschema==1.21.0
tenacity==9.0.0
thinc==8.3.6 thinc==8.3.6
tomli==2.2.1 threadpoolctl==3.6.0
tokenizers==0.22.1
toolz==1.0.0 toolz==1.0.0
torch==2.8.0
tqdm==4.67.1 tqdm==4.67.1
transformers==4.57.0
triton==3.4.0
typer==0.19.2 typer==0.19.2
types-python-dateutil==2.9.0.20251008 types-python-dateutil==2.9.0.20251008
typing-inspection==0.4.1 typing-inspection==0.4.2
typing_extensions==4.15.0 typing_extensions==4.15.0
tzdata==2025.2 tzdata==2025.2
unicodecsv==0.14.1 unicodecsv==0.14.1

View File

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

View File

@ -116,6 +116,7 @@
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</a> </a>
</li> {% endcomment %} </li> {% endcomment %}
<li class="nav-item me-2 d-none d-lg-block"> <li class="nav-item me-2 d-none d-lg-block">
{% if LANGUAGE_CODE == 'en' %} {% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline"> <form action="{% url 'set_language' %}" method="post" class="d-inline">
@ -135,23 +136,16 @@
</form> </form>
{% endif %} {% endif %}
</li> </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"> <li class="nav-item mx-3 d-none d-lg-block mt-2">
<a href="{% url 'message_list' %}" <a href="{% url 'message_list' %}" class="btn btn-sm btn-outline-warning position-relative">
class=" btn btn-sm btn-outline-warning position-relative">
<i class="fas fa-envelope me-1"></i> <i class="fas fa-envelope me-1"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"> <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ request.user.get_unread_message_count }} {{ request.user.get_unread_message_count }}
</span> </span>
</a> </a>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button <button
class="nav-link p-0 border-0 bg-transparent dropdown-toggle" class="nav-link p-0 border-0 bg-transparent dropdown-toggle"
@ -160,7 +154,7 @@
aria-expanded="false" aria-expanded="false"
aria-label="{% trans 'Toggle user menu' %}" aria-label="{% trans 'Toggle user menu' %}"
data-bs-auto-close="outside" 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 %} {% if user.profile_image %}
<img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar" <img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
@ -168,16 +162,17 @@
title="{% trans 'Your account' %}"> title="{% trans 'Your account' %}">
{% else %} {% else %}
<div class="profile-avatar" title="{% trans 'Your account' %}"> <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> </div>
{% endif %} {% endif %}
</button> </button>
<ul <ul class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3" style="min-width: 240px;">
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3" <li class="px-4 py-3">
style="min-width: 240px;"
>
<li class="px-4 py-3 ">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;"> <div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
{% if user.profile_image %} {% if user.profile_image %}
@ -197,67 +192,96 @@
</div> </div>
</div> </div>
</li> </li>
<li><hr class="dropdown-divider my-1"></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">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
<span class="me-2">🇸🇦</span> العربية
</button>
</form>
{% elif LANGUAGE_CODE == 'ar' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
<span class="me-2">🇺🇸</span> English
</button>
</form>
{% endif %}
</li>
<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="nav-item me-3 dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal d-lg-none">
{% if request.user.is_authenticated %} {% if LANGUAGE_CODE == 'en' %}
<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> <form action="{% url 'set_language' %}" method="post" class="d-inline">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
{% if request.user.is_superuser %} <button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
<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> <span class="me-2">🇸🇦</span> العربية
<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> </button>
<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> </form>
{% 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 %} {% elif LANGUAGE_CODE == 'ar' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
<span class="me-2">🇺🇸</span> English
</button>
</form>
{% endif %} {% 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>
{% 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>
{% 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 '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 %} {% endif %}
{% comment %} CORRECTED LINKEDIN BLOCK {% endcomment %} {% comment %} {% if not request.session.linkedin_authenticated %}
{% if not request.session.linkedin_authenticated %} <li>
<li> <a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
<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>
<i class="fab fa-linkedin me-3 text-primary fs-5"></i> <span>{% trans "Connect LinkedIn" %}</span>
<span>{% trans "Connect LinkedIn" %}</span> </a>
</a> </li>
</li>
{% else %} {% else %}
<li class="px-4 py-2 text-muted small"> <li class="px-4 py-2 text-muted small">
<i class="fab fa-linkedin text-primary me-2"></i> <i class="fab fa-linkedin text-primary me-2"></i>
{% trans "LinkedIn Connected" %} {% trans "LinkedIn Connected" %}
</li> </li>
{% endif %} {% endif %} {% endcomment %}
<li><hr class="dropdown-divider my-1"></li> <li><hr class="dropdown-divider my-1"></li>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li> <li>
<form method="post" action="{% url 'account_logout'%}" class="d-inline"> <form method="post" action="{% url 'account_logout'%}" class="d-inline">
{% csrf_token %} {% csrf_token %}
<button <button
type="submit" 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' %}" 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> <span style="color:red;">{% trans "Sign Out" %}</span>
</button> </button>
</form> </form>

View File

@ -1415,8 +1415,8 @@ const elements = {
</label> </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'); const input = document.createElement('input');
input.type = 'text'; input.type = 'text';
input.className = 'field-input'; input.className = 'field-input';

View File

@ -150,7 +150,7 @@
{% block content %} {% block content %}
<div class="container-fluid pt-5 pb-5 px-lg-5" style="background-color: var(--color-background-light);"> <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"> <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> </h1>
<div class="alert summary-alert border-start border-5 p-3 mb-5 mx-3" role="alert"> <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-confirmed { background-color: var(--kaauh-info) !important; color: white; }
.bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; } .bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; }
.bg-completed { background-color: var(--kaauh-success) !important; color: white; } .bg-completed { background-color: var(--kaauh-success) !important; color: white; }
.bg-remote { background-color: #007bff !important; color: white; } .bg-remote { background-color: #004a53 color: white; }
.bg-onsite { background-color: #6f42c1 !important; color: white; } .bg-onsite { background-color: #00636e !important; color: white; }
/* Timeline Styling */ /* Timeline Styling */
.timeline { .timeline {
@ -206,7 +206,6 @@
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <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"> <a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %} <i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
</a> </a>
{% if interview.status != 'cancelled' %}
<button type="button" class="btn btn-outline-secondary" <button type="button" class="btn btn-outline-secondary"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#statusModal"> data-bs-target="#statusModal">
<i class="fas fa-redo-alt me-1"></i> {% trans "Update Interview status" %} <i class="fas fa-redo-alt me-1"></i> {% trans "Update Interview status" %}
</button> </button>
{% endif %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- Left Column - Candidate & Interview Info -->
<div class="col-lg-8"> <div class="col-lg-8">
<!-- Candidate Information Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4"> <div class="kaauh-card shadow-sm p-4 mb-4">
<div class="d-flex align-items-start justify-content-between mb-3"> <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;"> <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 "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-2"><strong>{% trans "Applied Date:" %}</strong> {{ interview.application.created_at|date:"d-m-Y" }}</p>
<p class="mb-0"><strong>{% trans "Current Stage:" %}</strong> <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 }} {{ interview.application.stage }}
</span> </span>
</p> </p>
@ -285,7 +284,6 @@
</div> </div>
</div> </div>
<!-- Interview Details Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4"> <div class="kaauh-card shadow-sm p-4 mb-4">
<div class="d-flex align-items-center justify-content-between mb-3"> <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;"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
@ -384,7 +382,6 @@
</div> </div>
</div> </div>
<!-- Timeline/History Section -->
<div class="kaauh-card shadow-sm p-4"> <div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;"> <h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-history me-2"></i> {% trans "Interview Timeline" %} <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 class="d-flex justify-content-between align-items-start">
<div> <div>
<h6 class="mb-1">{% trans "Interview Scheduled" %}</h6> <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> </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> </div>
</div> </div>
{% if interview.interview.status == 'CONFIRMED' %} {% if interview.status == 'confirmed' %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content"> <div class="timeline-content">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@ -414,7 +411,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if interview.interview.status == 'COMPLETED' %} {% if interview.status == 'completed' %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content"> <div class="timeline-content">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@ -427,15 +424,15 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if interview.interview.status == 'CANCELLED' %} {% if interview.status == 'cancelled' %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content"> <div class="timeline-content">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div> <div>
<h6 class="mb-1">{% trans "Interview Cancelled" %}</h6> <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> </div>
<small class="text-muted">{% trans "Recently" %}</small>
</div> </div>
</div> </div>
</div> </div>
@ -444,15 +441,12 @@
</div> </div>
</div> </div>
<!-- Right Column - Participants & Actions -->
<div class="col-lg-4"> <div class="col-lg-4">
<!-- Participants Panel --> {% comment %} <div class="kaauh-card shadow-sm p-4 mb-4">
<div class="kaauh-card shadow-sm p-4 mb-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;"> <h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-users me-2"></i> {% trans "Participants" %} <i class="fas fa-users me-2"></i> {% trans "Participants" %}
</h5> </h5>
<!-- Internal Participants -->
{% if interview.participants.exists %} {% if interview.participants.exists %}
<h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6> <h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6>
{% for participant in interview.participants.all %} {% for participant in interview.participants.all %}
@ -468,7 +462,6 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<!-- External Participants -->
{% if interview.system_users.exists %} {% if interview.system_users.exists %}
<h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6> <h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6>
{% for user in interview.system_users.all %} {% for user in interview.system_users.all %}
@ -496,49 +489,71 @@
data-bs-target="#participantModal"> data-bs-target="#participantModal">
<i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %} <i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %}
</button> </button>
</div> </div> {% endcomment %}
<!-- Actions Panel -->
<div class="kaauh-card shadow-sm p-4"> <div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;"> <h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-cog me-2"></i> {% trans "Actions" %} <i class="fas fa-cog me-2"></i> {% trans "Actions" %}
</h5> </h5>
<div class="action-buttons"> <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" <button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#rescheduleModal"> data-bs-target="#rescheduleModal">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %} <i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button> </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-toggle="modal"
data-bs-target="#cancelModal"> 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> </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 %} {% 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-toggle="modal"
data-bs-target="#emailModal"> data-bs-target="#emailModal">
<i class="fas fa-envelope me-1"></i> {% trans "Send Email" %} <i class="fas fa-envelope me-1"></i> {% trans "Send Email" %}
</button> </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> </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 fade modal-xl" id="candidateModal" tabindex="-1" aria-labelledby="candidateModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -558,7 +573,6 @@
</div> </div>
</div> </div>
<!-- Participant Modal -->
<div class="modal fade" id="participantModal" tabindex="-1" aria-labelledby="participantModalLabel" aria-hidden="true"> <div class="modal fade" id="participantModal" tabindex="-1" aria-labelledby="participantModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -574,14 +588,12 @@
<div class="mb-3"> <div class="mb-3">
<label for="internal_participants" class="form-label">{% trans "Internal Participants" %}</label> <label for="internal_participants" class="form-label">{% trans "Internal Participants" %}</label>
<select multiple class="form-select" id="internal_participants" name="participants"> <select multiple class="form-select" id="internal_participants" name="participants">
<!-- Options will be populated dynamically --> </select>
</select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="external_participants" class="form-label">{% trans "External Participants" %}</label> <label for="external_participants" class="form-label">{% trans "External Participants" %}</label>
<select multiple class="form-select" id="external_participants" name="system_users"> <select multiple class="form-select" id="external_participants" name="system_users">
<!-- Options will be populated dynamically --> </select>
</select>
</div> </div>
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-plus me-1"></i> {% trans "Add Participants" %} <i class="fas fa-plus me-1"></i> {% trans "Add Participants" %}
@ -592,7 +604,6 @@
</div> </div>
</div> </div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true"> <div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -616,23 +627,23 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email_message" class="form-label">{% trans "Message" %}</label> <label for="email_message" class="form-label">{% trans "Message" %}</label>
<textarea class="form-control" id="email_message" name="message" rows="6"> <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 "Date:" %} {{ interview.interview_date|date:"d-m-Y" }}
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }} {% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }}
{% trans "Job:" %} {{ interview.job.title }} {% 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." %} {% 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." %} {% trans "This is an onsite interview. Please arrive 10 minutes early." %}
{% endif %} {% endif %}
{% trans "Best regards," %} {% trans "Best regards," %}
{% trans "HR Team" %} {% trans "HR Team" %}
</textarea> </textarea>
</div> </div>
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
@ -644,7 +655,6 @@
</div> </div>
</div> </div>
<!-- Reschedule Modal -->
<div class="modal fade" id="rescheduleModal" tabindex="-1" aria-labelledby="rescheduleModalLabel" aria-hidden="true"> <div class="modal fade" id="rescheduleModal" tabindex="-1" aria-labelledby="rescheduleModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -657,20 +667,9 @@
<div class="modal-body"> <div class="modal-body">
<form method="post" action="{% url 'reschedule_meeting_for_application' interview.slug %}"> <form method="post" action="{% url 'reschedule_meeting_for_application' interview.slug %}">
{% csrf_token %} {% csrf_token %}
{{reschedule_form|crispy}} {{ reschedule_form|crispy }}
{% comment %} <div class="mb-3">
<label for="topic" class="form-label">{% trans "topic" %}</label> <button type="submit" class="btn btn-main-action btn-sm mt-3">
<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">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %} <i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button> </button>
</form> </form>
@ -679,7 +678,6 @@
</div> </div>
</div> </div>
<!-- Cancel Modal -->
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true"> <div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -690,19 +688,20 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <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 %} {% csrf_token %}
{{ cancel_form|crispy }}
<div class="alert alert-warning"> <div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i> <i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Are you sure you want to cancel this interview? This action cannot be undone." %} {% trans "Are you sure you want to cancel this interview? This action cannot be undone." %}
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2 justify-content-end">
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal"> <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
{% trans "Close" %} {% trans "Close" %}
</button> </button>
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
</button>
</div> </div>
</form> </form>
</div> </div>
@ -710,7 +709,6 @@
</div> </div>
</div> </div>
<!-- Result Modal -->
<div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true"> <div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -737,7 +735,7 @@
<textarea class="form-control" id="result_notes" name="notes" rows="4" <textarea class="form-control" id="result_notes" name="notes" rows="4"
placeholder="{% trans 'Add interview feedback and notes' %}"></textarea> placeholder="{% trans 'Add interview feedback and notes' %}"></textarea>
</div> </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" %} <i class="fas fa-check me-1"></i> {% trans "Update Result" %}
</button> </button>
</form> </form>
@ -746,7 +744,6 @@
</div> </div>
</div> </div>
<!-- Update Status Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true"> <div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
@ -759,14 +756,14 @@
<div class="modal-body"> <div class="modal-body">
<form method="post" action="{% url 'update_interview_status' interview.slug %}"> <form method="post" action="{% url 'update_interview_status' interview.slug %}">
{% csrf_token %} {% csrf_token %}
{{interview_status_form|crispy}} {{ interview_status_form|crispy }}
<div class="d-flex gap-2"> <div class="d-flex gap-2 mt-3 justify-content-end">
<button type="submit" class="btn btn-main-action"> <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
{% trans "Close" %} {% trans "Close" %}
</button> </button>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
</button>
</div> </div>
</form> </form>
</div> </div>
@ -780,7 +777,7 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Clear modal content when hidden // 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 => { modals.forEach(modalId => {
const modal = document.getElementById(modalId); const modal = document.getElementById(modalId);
@ -788,6 +785,7 @@ document.addEventListener('DOMContentLoaded', function () {
modal.addEventListener('hidden.bs.modal', function () { modal.addEventListener('hidden.bs.modal', function () {
const modalBody = modal.querySelector('.modal-body'); const modalBody = modal.querySelector('.modal-body');
if (modalBody && modalId === 'candidateModal') { if (modalBody && modalId === 'candidateModal') {
// Reset content for AJAX-loaded Candidate Modal
modalBody.innerHTML = ` modalBody.innerHTML = `
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br> <i class="fas fa-spinner fa-spin fa-2x"></i><br>
@ -795,9 +793,12 @@ document.addEventListener('DOMContentLoaded', function () {
</div> </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.
}); });
} }
}); });
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -509,6 +509,17 @@
</div> </div>
</div> </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>
</div> </div>
{% endif %} {% 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 %}