encrpytion removed #82
@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
||||
"django_q",
|
||||
"widget_tweaks",
|
||||
"easyaudit",
|
||||
"encrypted_model_fields",
|
||||
]
|
||||
|
||||
|
||||
@ -530,4 +531,7 @@ LOGGING={
|
||||
"style": "{",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FIELD_ENCRYPTION_KEY="PWQimxxcDjlRsSSof2gaj42a3frmrLt2xgCTa4R06pE="
|
||||
@ -6,7 +6,7 @@ from .models import (
|
||||
JobPosting, Application, TrainingMaterial,
|
||||
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
|
||||
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
|
||||
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview,Person
|
||||
)
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
@ -250,4 +250,5 @@ admin.site.register(ScheduledInterview)
|
||||
|
||||
|
||||
admin.site.register(JobPostingImage)
|
||||
admin.site.register(Person)
|
||||
# admin.site.register(User)
|
||||
|
||||
@ -6,6 +6,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
def job_not_expired(view_func):
|
||||
@wraps(view_func)
|
||||
@ -162,3 +163,12 @@ def staff_or_agency_required(view_func):
|
||||
def staff_or_candidate_required(view_func):
|
||||
"""Decorator to restrict view to staff and candidate users."""
|
||||
return user_type_required(['staff', 'candidate'], login_url='/accounts/login/')(view_func)
|
||||
|
||||
|
||||
def is_superuser(user):
|
||||
|
||||
return user.is_authenticated and user.is_superuser
|
||||
|
||||
|
||||
def superuser_required(view_func):
|
||||
return user_passes_test(is_superuser, login_url='/admin/login/?next=/', redirect_field_name=None)(view_func)
|
||||
@ -28,7 +28,8 @@ from .models import (
|
||||
Message,
|
||||
Person,
|
||||
Document,
|
||||
CustomUser
|
||||
CustomUser,
|
||||
Interview
|
||||
)
|
||||
|
||||
# from django_summernote.widgets import SummernoteWidget
|
||||
@ -270,7 +271,7 @@ class SourceAdvancedForm(forms.ModelForm):
|
||||
class PersonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","gender","address"]
|
||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","gpa","national_id","nationality","gender","address"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@ -281,6 +282,8 @@ class PersonForm(forms.ModelForm):
|
||||
"date_of_birth": forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
|
||||
"address": forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
"gpa": forms.TextInput(attrs={'class': 'custom-decimal-input'}),
|
||||
"national_id":forms.NumberInput(attrs={'min': 0, 'step': 1}),
|
||||
}
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
|
||||
@ -790,11 +793,36 @@ class BulkInterviewTemplateForm(forms.ModelForm):
|
||||
self.fields["applications"].queryset = Application.objects.filter(
|
||||
job__slug=slug, stage="Interview"
|
||||
)
|
||||
self.fields["topic"].initial = "Interview for " + str(
|
||||
self.fields["applications"].queryset.first().job.title
|
||||
)
|
||||
self.fields["start_date"].initial = timezone.now().date()
|
||||
working_days_initial = [0, 1, 2, 3, 6] # Monday to Friday
|
||||
self.fields["working_days"].initial = working_days_initial
|
||||
self.fields["start_time"].initial = "08:00"
|
||||
self.fields["end_time"].initial = "14:00"
|
||||
self.fields["interview_duration"].initial = 30
|
||||
self.fields["buffer_time"].initial = 10
|
||||
self.fields["break_start_time"].initial = "11:30"
|
||||
self.fields["break_end_time"].initial = "12:00"
|
||||
self.fields["physical_address"].initial = "Airport Road, King Khalid International Airport, Riyadh 11564, Saudi Arabia"
|
||||
|
||||
def clean_working_days(self):
|
||||
working_days = self.cleaned_data.get("working_days")
|
||||
return [int(day) for day in working_days]
|
||||
|
||||
class InterviewCancelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Interview
|
||||
fields = ["cancelled_reason","cancelled_at"]
|
||||
widgets = {
|
||||
"cancelled_reason": forms.Textarea(
|
||||
attrs={"class": "form-control", "rows": 3}
|
||||
),
|
||||
"cancelled_at": forms.DateTimeInput(
|
||||
attrs={"class": "form-control", "type": "datetime-local"}
|
||||
),
|
||||
}
|
||||
|
||||
class NoteForm(forms.ModelForm):
|
||||
"""Form for creating and editing meeting comments"""
|
||||
@ -2292,18 +2320,19 @@ class ApplicantSignupForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ["first_name","middle_name","last_name", "email","phone","gpa","nationality", "date_of_birth","gender","address"]
|
||||
fields = ["first_name","middle_name","last_name", "email","phone","gpa","nationality","national_id", "date_of_birth","gender","address"]
|
||||
widgets = {
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'middle_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
# 'gpa': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'gpa': forms.TextInput(attrs={'class': 'custom-decimal-input'}),
|
||||
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
|
||||
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'gender': forms.Select(attrs={'class': 'form-control'}),
|
||||
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'national_id':forms.NumberInput(attrs={'min': 0, 'step': 1}),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
@ -2878,4 +2907,17 @@ class ScheduledInterviewUpdateStatusForm(forms.Form):
|
||||
)
|
||||
class Meta:
|
||||
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
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.7 on 2025-12-02 14:21
|
||||
# Generated by Django 5.2.7 on 2025-12-07 13:15
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
@ -8,6 +8,7 @@ import django.utils.timezone
|
||||
import django_ckeditor_5.fields
|
||||
import django_countries.fields
|
||||
import django_extensions.db.fields
|
||||
import encrypted_model_fields.fields
|
||||
import recruitment.validators
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@ -73,6 +74,8 @@ class Migration(migrations.Migration):
|
||||
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
|
||||
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
|
||||
('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)),
|
||||
('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='Cancelled At')),
|
||||
('cancelled_reason', models.TextField(blank=True, null=True, verbose_name='Cancellation Reason')),
|
||||
('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')),
|
||||
('password', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('zoom_gateway_response', models.JSONField(blank=True, null=True)),
|
||||
@ -150,10 +153,10 @@ class Migration(migrations.Migration):
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
|
||||
('phone', encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Phone')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
|
||||
('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')),
|
||||
('email', models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)),
|
||||
('email', encrypted_model_fields.fields.EncryptedEmailField(error_messages={'unique': 'A user with this email already exists.'}, unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
@ -484,13 +487,14 @@ class Migration(migrations.Migration):
|
||||
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||
('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')),
|
||||
('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
|
||||
('email', encrypted_model_fields.fields.EncryptedEmailField(db_index=True, unique=True, verbose_name='Email')),
|
||||
('phone', encrypted_model_fields.fields.EncryptedPositiveIntegerField(blank=True, null=True, verbose_name='Phone')),
|
||||
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||
('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')),
|
||||
('gpa', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='GPA')),
|
||||
('gpa', models.DecimalField(decimal_places=2, help_text='GPA must be between 0 and 4.', max_digits=3, verbose_name='GPA')),
|
||||
('national_id', encrypted_model_fields.fields.EncryptedPositiveIntegerField(help_text='Enter the national id or iqama number')),
|
||||
('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')),
|
||||
('address', models.TextField(blank=True, null=True, verbose_name='Address')),
|
||||
('address', encrypted_model_fields.fields.EncryptedCharField(blank=True, null=True, verbose_name='Address')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
|
||||
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
|
||||
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||
|
||||
19
recruitment/migrations/0002_alter_person_address.py
Normal file
19
recruitment/migrations/0002_alter_person_address.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -21,6 +21,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import F, Value, IntegerField, CharField,Q
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.db.models.fields.json import KeyTransform, KeyTextTransform
|
||||
from encrypted_model_fields.fields import EncryptedCharField,EncryptedEmailField,EncryptedTextField
|
||||
|
||||
class EmailContent(models.Model):
|
||||
subject = models.CharField(max_length=255, verbose_name=_("Subject"))
|
||||
@ -46,8 +47,8 @@ class CustomUser(AbstractUser):
|
||||
user_type = models.CharField(
|
||||
max_length=20, choices=USER_TYPES, default="staff", verbose_name=_("User Type")
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=20, blank=True, null=True, verbose_name=_("Phone")
|
||||
phone = EncryptedCharField(
|
||||
blank=True, null=True, verbose_name=_("Phone")
|
||||
)
|
||||
profile_image = models.ImageField(
|
||||
null=True,
|
||||
@ -59,7 +60,7 @@ class CustomUser(AbstractUser):
|
||||
designation = models.CharField(
|
||||
max_length=100, blank=True, null=True, verbose_name=_("Designation")
|
||||
)
|
||||
email = models.EmailField(
|
||||
email = EncryptedEmailField(
|
||||
unique=True,
|
||||
error_messages={
|
||||
"unique": _("A user with this email already exists."),
|
||||
@ -73,7 +74,7 @@ class CustomUser(AbstractUser):
|
||||
@property
|
||||
def get_unread_message_count(self):
|
||||
message_list = (
|
||||
Message.objects.filter(Q(sender=self) | Q(recipient=self), is_read=False)
|
||||
Message.objects.filter(Q(recipient=self), is_read=False)
|
||||
)
|
||||
return message_list.count() or 0
|
||||
|
||||
@ -516,13 +517,13 @@ class Person(Base):
|
||||
middle_name = models.CharField(
|
||||
max_length=255, blank=True, null=True, verbose_name=_("Middle Name")
|
||||
)
|
||||
email = models.EmailField(
|
||||
email = EncryptedEmailField(
|
||||
unique=True,
|
||||
db_index=True,
|
||||
verbose_name=_("Email"),
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=20, blank=True, null=True, verbose_name=_("Phone")
|
||||
phone = EncryptedCharField(
|
||||
blank=True, null=True, verbose_name=_("Phone")
|
||||
)
|
||||
date_of_birth = models.DateField(
|
||||
null=True, blank=True, verbose_name=_("Date of Birth")
|
||||
@ -535,10 +536,13 @@ class Person(Base):
|
||||
verbose_name=_("Gender"),
|
||||
)
|
||||
gpa = models.DecimalField(
|
||||
max_digits=3, decimal_places=2, blank=True, null=True, verbose_name=_("GPA")
|
||||
max_digits=3, decimal_places=2, verbose_name=_("GPA"),help_text=_("GPA must be between 0 and 4.")
|
||||
)
|
||||
national_id = EncryptedCharField(
|
||||
help_text=_("Enter the national id or iqama number")
|
||||
)
|
||||
nationality = CountryField(blank=True, null=True, verbose_name=_("Nationality"))
|
||||
address = models.TextField(blank=True, null=True, verbose_name=_("Address"))
|
||||
address = EncryptedTextField(blank=True, null=True, verbose_name=_("Address"))
|
||||
|
||||
# Optional linking to user account
|
||||
user = models.OneToOneField(
|
||||
@ -1336,6 +1340,8 @@ class Interview(Base):
|
||||
default=Status.WAITING,
|
||||
db_index=True
|
||||
)
|
||||
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Cancelled At"))
|
||||
cancelled_reason = models.TextField(blank=True, null=True, verbose_name=_("Cancellation Reason"))
|
||||
|
||||
# Remote-specific (nullable)
|
||||
meeting_id = models.CharField(
|
||||
@ -1784,7 +1790,7 @@ class FormSubmission(Base):
|
||||
applicant_name = models.CharField(
|
||||
max_length=200, blank=True, help_text="Name of the applicant"
|
||||
)
|
||||
applicant_email = models.EmailField(
|
||||
applicant_email = EncryptedEmailField(
|
||||
db_index=True, blank=True, help_text="Email of the applicant"
|
||||
) # Added index
|
||||
|
||||
@ -2086,8 +2092,8 @@ class HiringAgency(Base):
|
||||
contact_person = models.CharField(
|
||||
max_length=150, blank=True, verbose_name=_("Contact Person")
|
||||
)
|
||||
email = models.EmailField(blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
email = EncryptedEmailField(blank=True)
|
||||
phone = EncryptedCharField(max_length=20, blank=True)
|
||||
website = models.URLField(blank=True)
|
||||
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
|
||||
country = CountryField(blank=True, null=True, blank_label=_("Select country"))
|
||||
@ -2497,8 +2503,8 @@ class Participants(Base):
|
||||
name = models.CharField(
|
||||
max_length=255, verbose_name=_("Participant Name"), null=True, blank=True
|
||||
)
|
||||
email = models.EmailField(verbose_name=_("Email"))
|
||||
phone = models.CharField(
|
||||
email =EncryptedEmailField(verbose_name=_("Email"))
|
||||
phone = EncryptedCharField(
|
||||
max_length=12, verbose_name=_("Phone Number"), null=True, blank=True
|
||||
)
|
||||
designation = models.CharField(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
import random
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from django.db import transaction
|
||||
from django_q.models import Schedule
|
||||
@ -437,12 +437,8 @@ def notification_created(sender, instance, created, **kwargs):
|
||||
logger.info(f"Notification cached for SSE: {notification_data}")
|
||||
|
||||
|
||||
def generate_random_password():
|
||||
import string
|
||||
|
||||
return "".join(random.choices(string.ascii_letters + string.digits, k=12))
|
||||
|
||||
|
||||
from .utils import generate_random_password
|
||||
@receiver(post_save, sender=HiringAgency)
|
||||
def hiring_agency_created(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
|
||||
@ -283,7 +283,8 @@ urlpatterns = [
|
||||
name="user_profile_image_update",
|
||||
),
|
||||
path("easy_logs/", views.easy_logs, name="easy_logs"),
|
||||
path("settings/", views.admin_settings, name="admin_settings"),
|
||||
path('settings/',views.settings,name="settings"),
|
||||
path("settings/admin/", views.admin_settings, name="admin_settings"),
|
||||
path("staff/create", views.create_staff_user, name="create_staff_user"),
|
||||
path(
|
||||
"set_staff_password/<int:pk>/",
|
||||
@ -353,6 +354,8 @@ urlpatterns = [
|
||||
# ),
|
||||
# Hiring Agency URLs
|
||||
path("agencies/", views.agency_list, name="agency_list"),
|
||||
path("regenerate_agency_password/<slug:slug>/", views.regenerate_agency_password, name="regenerate_agency_password"),
|
||||
path("deactivate_agency/<slug:slug>/", views.deactivate_agency, name="deactivate_agency"),
|
||||
path("agencies/create/", views.agency_create, name="agency_create"),
|
||||
path("agencies/<slug:slug>/", views.agency_detail, name="agency_detail"),
|
||||
path("agencies/<slug:slug>/update/", views.agency_update, name="agency_update"),
|
||||
|
||||
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||
from .models import ScheduledInterview
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import send_mail
|
||||
import random
|
||||
# nlp = spacy.load("en_core_web_sm")
|
||||
|
||||
# def extract_text_from_pdf(pdf_path):
|
||||
@ -617,3 +618,8 @@ def update_meeting(instance, updated_data):
|
||||
|
||||
|
||||
|
||||
|
||||
def generate_random_password():
|
||||
import string
|
||||
|
||||
return "".join(random.choices(string.ascii_letters + string.digits, k=12))
|
||||
@ -23,6 +23,7 @@ from .decorators import (
|
||||
StaffRequiredMixin,
|
||||
StaffOrAgencyRequiredMixin,
|
||||
StaffOrCandidateRequiredMixin,
|
||||
superuser_required
|
||||
)
|
||||
from .forms import (
|
||||
StaffUserCreationForm,
|
||||
@ -38,8 +39,10 @@ from .forms import (
|
||||
StaffAssignmentForm,
|
||||
RemoteInterviewForm,
|
||||
OnsiteInterviewForm,
|
||||
BulkInterviewTemplateForm
|
||||
BulkInterviewTemplateForm,
|
||||
InterviewCancelForm
|
||||
)
|
||||
from .utils import generate_random_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
@ -159,8 +162,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@login_required
|
||||
def settings(request):
|
||||
return render(request,'user/settings.html')
|
||||
|
||||
class PersonListView(StaffRequiredMixin, ListView):
|
||||
|
||||
class PersonListView(StaffRequiredMixin, ListView,LoginRequiredMixin):
|
||||
model = Person
|
||||
template_name = "people/person_list.html"
|
||||
context_object_name = "people_list"
|
||||
@ -197,7 +204,7 @@ class PersonListView(StaffRequiredMixin, ListView):
|
||||
|
||||
|
||||
|
||||
class PersonCreateView(CreateView):
|
||||
class PersonCreateView(CreateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin):
|
||||
model = Person
|
||||
template_name = "people/create_person.html"
|
||||
form_class = PersonForm
|
||||
@ -221,13 +228,13 @@ class PersonCreateView(CreateView):
|
||||
|
||||
|
||||
|
||||
class PersonDetailView(DetailView):
|
||||
class PersonDetailView(DetailView,LoginRequiredMixin,StaffRequiredMixin):
|
||||
model = Person
|
||||
template_name = "people/person_detail.html"
|
||||
context_object_name = "person"
|
||||
|
||||
|
||||
class PersonUpdateView( UpdateView):
|
||||
class PersonUpdateView( UpdateView,LoginRequiredMixin,StaffOrAgencyRequiredMixin):
|
||||
model = Person
|
||||
template_name = "people/update_person.html"
|
||||
form_class = PersonForm
|
||||
@ -239,7 +246,7 @@ class PersonUpdateView( UpdateView):
|
||||
return redirect("agency_portal_persons_list")
|
||||
return super().form_valid(form)
|
||||
|
||||
class PersonDeleteView(StaffRequiredMixin, DeleteView):
|
||||
class PersonDeleteView(StaffRequiredMixin, DeleteView,LoginRequiredMixin):
|
||||
model = Person
|
||||
template_name = "people/delete_person.html"
|
||||
success_url = reverse_lazy("person_list")
|
||||
@ -488,7 +495,9 @@ from django.db.models.functions import Coalesce, Cast # Coalesce handles NULLs
|
||||
from django.db.models import Avg, IntegerField, Value # Value is used for the default '0'
|
||||
# These are essential for safely querying PostgreSQL JSONB fields
|
||||
from django.db.models.fields.json import KeyTransform, KeyTextTransform
|
||||
|
||||
@staff_user_required
|
||||
@login_required
|
||||
def job_detail(request, slug):
|
||||
"""View details of a specific job"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
@ -711,7 +720,8 @@ def job_detail(request, slug):
|
||||
# )
|
||||
|
||||
# return response
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def request_cvs_download(request, slug):
|
||||
"""
|
||||
View to initiate the background task.
|
||||
@ -735,6 +745,8 @@ def request_cvs_download(request, slug):
|
||||
messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.")
|
||||
return redirect('job_detail', slug=slug) # Redirect back to the job detail page
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def download_ready_cvs(request, slug):
|
||||
"""
|
||||
View to serve the file once it is ready.
|
||||
@ -985,12 +997,13 @@ def linkedin_callback(request):
|
||||
|
||||
|
||||
# applicant views
|
||||
def applicant_job_detail(request, slug):
|
||||
"""View job details for applicants"""
|
||||
job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE")
|
||||
return render(request, "jobs/applicant_job_detail.html", {"job": job})
|
||||
|
||||
# def applicant_job_detail(request, slug):
|
||||
# """View job details for applicants"""
|
||||
# job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE")
|
||||
# return render(request, "jobs/applicant_job_detail.html", {"job": job})
|
||||
|
||||
@login_required
|
||||
@candidate_user_required
|
||||
def application_success(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
return render(request, "jobs/application_success.html", {"job": job})
|
||||
@ -1012,6 +1025,8 @@ def form_builder(request, template_slug=None):
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def save_form_template(request):
|
||||
"""Save a new or existing form template"""
|
||||
try:
|
||||
@ -1073,6 +1088,8 @@ def save_form_template(request):
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def load_form_template(request, template_slug):
|
||||
"""Load an existing form template"""
|
||||
template = get_object_or_404(FormTemplate, slug=template_slug)
|
||||
@ -1227,7 +1244,8 @@ def delete_form_template(request, template_id):
|
||||
)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_or_candidate_required
|
||||
def application_submit_form(request, template_slug):
|
||||
"""Display the form as a step-by-step wizard"""
|
||||
if not request.user.is_authenticated:
|
||||
@ -1282,7 +1300,10 @@ def application_submit_form(request, template_slug):
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
@login_required
|
||||
@candidate_user_required
|
||||
def application_submit(request, template_slug):
|
||||
import re
|
||||
"""Handle form submission"""
|
||||
if not request.user.is_authenticated :# or request.user.user_type != "candidate":
|
||||
return JsonResponse({"success": False, "message": "Unauthorized access."})
|
||||
@ -1345,6 +1366,29 @@ def application_submit(request, template_slug):
|
||||
# phone = submission.responses.get(field__label="Phone Number")
|
||||
# address = submission.responses.get(field__label="Address")
|
||||
gpa = submission.responses.get(field__label="GPA")
|
||||
if gpa and gpa.value:
|
||||
gpa_str = gpa.value.replace("/","").strip()
|
||||
|
||||
if not re.match(r'^\d+(\.\d+)?$', gpa_str):
|
||||
# --- FIX APPLIED HERE ---
|
||||
return JsonResponse(
|
||||
{"success": False, "message": _("GPA must be a numeric value.")}
|
||||
)
|
||||
|
||||
try:
|
||||
gpa_float = float(gpa_str)
|
||||
except ValueError:
|
||||
# --- FIX APPLIED HERE ---
|
||||
return JsonResponse(
|
||||
{"success": False, "message": _("GPA must be a numeric value.")}
|
||||
)
|
||||
|
||||
if not (0.0 <= gpa_float <= 4.0):
|
||||
# --- FIX APPLIED HERE ---
|
||||
return JsonResponse(
|
||||
{"success": False, "message": _("GPA must be between 0.0 and 4.0.")}
|
||||
)
|
||||
|
||||
|
||||
resume = submission.responses.get(field__label="Resume Upload")
|
||||
|
||||
@ -1450,6 +1494,7 @@ def form_template_all_submissions(request, template_id):
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def form_submission_details(request, template_id, slug):
|
||||
"""Display detailed view of a specific form submission"""
|
||||
# Get the form template and verify ownership
|
||||
@ -1484,6 +1529,8 @@ def form_submission_details(request, template_id, slug):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def _handle_get_request(request, slug, job):
|
||||
"""
|
||||
Handles GET requests, setting up forms and restoring candidate selections
|
||||
@ -1520,7 +1567,8 @@ def _handle_get_request(request, slug, job):
|
||||
)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def _handle_preview_submission(request, slug, job):
|
||||
"""
|
||||
Handles the initial POST request (Preview Schedule).
|
||||
@ -1642,6 +1690,8 @@ def _handle_preview_submission(request, slug, job):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def _handle_confirm_schedule(request, slug, job):
|
||||
"""
|
||||
Handles the final POST request (Confirm Schedule).
|
||||
@ -1754,6 +1804,8 @@ def _handle_confirm_schedule(request, slug, job):
|
||||
return redirect("schedule_interviews", slug=slug)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
if request.method == "POST":
|
||||
@ -1766,6 +1818,8 @@ def schedule_interviews_view(request, slug):
|
||||
# return redirect("applications_interview_view", slug=slug)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def confirm_schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
if request.method == "POST":
|
||||
@ -1773,6 +1827,7 @@ def confirm_schedule_interviews_view(request, slug):
|
||||
return _handle_confirm_schedule(request, slug, job)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def applications_screening_view(request, slug):
|
||||
"""
|
||||
@ -1855,6 +1910,7 @@ def applications_screening_view(request, slug):
|
||||
return render(request, "recruitment/applications_screening_view.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def applications_exam_view(request, slug):
|
||||
"""
|
||||
@ -1865,6 +1921,7 @@ def applications_exam_view(request, slug):
|
||||
return render(request, "recruitment/applications_exam_view.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def update_application_exam_status(request, slug):
|
||||
application = get_object_or_404(Application, slug=slug)
|
||||
@ -1881,7 +1938,7 @@ def update_application_exam_status(request, slug):
|
||||
{"application": application, "form": form},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def bulk_update_application_exam_status(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
@ -1901,13 +1958,15 @@ def bulk_update_application_exam_status(request, slug):
|
||||
return redirect("applications_exam_view", slug=job.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def application_criteria_view_htmx(request, pk):
|
||||
application = get_object_or_404(Application, pk=pk)
|
||||
return render(
|
||||
request, "includes/application_modal_body.html", {"application": application}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def application_set_exam_date(request, slug):
|
||||
application = get_object_or_404(Application, slug=slug)
|
||||
@ -1918,7 +1977,7 @@ def application_set_exam_date(request, slug):
|
||||
)
|
||||
return redirect("applications_screening_view", slug=application.job.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def application_update_status(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
@ -1998,6 +2057,7 @@ def application_update_status(request, slug):
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def applications_interview_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
@ -2010,6 +2070,7 @@ def applications_interview_view(request, slug):
|
||||
return render(request, "recruitment/applications_interview_view.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def applications_document_review_view(request, slug):
|
||||
"""
|
||||
@ -2036,7 +2097,7 @@ def applications_document_review_view(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/applications_document_review_view.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@staff_user_required
|
||||
def reschedule_meeting_for_application(request, slug):
|
||||
@ -2150,62 +2211,62 @@ def reschedule_meeting_for_application(request, slug):
|
||||
|
||||
# @staff_user_required
|
||||
# def interview_calendar_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
# job = get_object_or_404(JobPosting, slug=slug)
|
||||
|
||||
# Get all scheduled interviews for this job
|
||||
scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
|
||||
"applicaton", "zoom_meeting"
|
||||
)
|
||||
# # Get all scheduled interviews for this job
|
||||
# scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
|
||||
# "applicaton", "zoom_meeting"
|
||||
# )
|
||||
|
||||
# Convert interviews to calendar events
|
||||
events = []
|
||||
for interview in scheduled_interviews:
|
||||
# Create start datetime
|
||||
start_datetime = datetime.combine(
|
||||
interview.interview_date, interview.interview_time
|
||||
)
|
||||
# # Convert interviews to calendar events
|
||||
# events = []
|
||||
# for interview in scheduled_interviews:
|
||||
# # Create start datetime
|
||||
# start_datetime = datetime.combine(
|
||||
# interview.interview_date, interview.interview_time
|
||||
# )
|
||||
|
||||
# Calculate end datetime based on interview duration
|
||||
duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60
|
||||
end_datetime = start_datetime + timedelta(minutes=duration)
|
||||
# # Calculate end datetime based on interview duration
|
||||
# duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60
|
||||
# end_datetime = start_datetime + timedelta(minutes=duration)
|
||||
|
||||
# Determine event color based on status
|
||||
color = "#00636e" # Default color
|
||||
if interview.status == "confirmed":
|
||||
color = "#00a86b" # Green for confirmed
|
||||
elif interview.status == "cancelled":
|
||||
color = "#e74c3c" # Red for cancelled
|
||||
elif interview.status == "completed":
|
||||
color = "#95a5a6" # Gray for completed
|
||||
# # Determine event color based on status
|
||||
# color = "#00636e" # Default color
|
||||
# if interview.status == "confirmed":
|
||||
# color = "#00a86b" # Green for confirmed
|
||||
# elif interview.status == "cancelled":
|
||||
# color = "#e74c3c" # Red for cancelled
|
||||
# elif interview.status == "completed":
|
||||
# color = "#95a5a6" # Gray for completed
|
||||
|
||||
events.append(
|
||||
{
|
||||
"title": f"Interview: {interview.candidate.name}",
|
||||
"start": start_datetime.isoformat(),
|
||||
"end": end_datetime.isoformat(),
|
||||
"url": f"{request.path}interview/{interview.id}/",
|
||||
"color": color,
|
||||
"extendedProps": {
|
||||
"candidate": interview.candidate.name,
|
||||
"email": interview.candidate.email,
|
||||
"status": interview.status,
|
||||
"meeting_id": interview.zoom_meeting.meeting_id
|
||||
if interview.zoom_meeting
|
||||
else None,
|
||||
"join_url": interview.zoom_meeting.join_url
|
||||
if interview.zoom_meeting
|
||||
else None,
|
||||
},
|
||||
}
|
||||
)
|
||||
# events.append(
|
||||
# {
|
||||
# "title": f"Interview: {interview.candidate.name}",
|
||||
# "start": start_datetime.isoformat(),
|
||||
# "end": end_datetime.isoformat(),
|
||||
# "url": f"{request.path}interview/{interview.id}/",
|
||||
# "color": color,
|
||||
# "extendedProps": {
|
||||
# "candidate": interview.candidate.name,
|
||||
# "email": interview.candidate.email,
|
||||
# "status": interview.status,
|
||||
# "meeting_id": interview.zoom_meeting.meeting_id
|
||||
# if interview.zoom_meeting
|
||||
# else None,
|
||||
# "join_url": interview.zoom_meeting.join_url
|
||||
# if interview.zoom_meeting
|
||||
# else None,
|
||||
# },
|
||||
# }
|
||||
# )
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"events": events,
|
||||
"calendar_color": "#00636e",
|
||||
}
|
||||
# context = {
|
||||
# "job": job,
|
||||
# "events": events,
|
||||
# "calendar_color": "#00636e",
|
||||
# }
|
||||
|
||||
return render(request, "recruitment/interview_calendar.html", context)
|
||||
# return render(request, "recruitment/interview_calendar.html", context)
|
||||
|
||||
|
||||
# @staff_user_required
|
||||
@ -2890,7 +2951,7 @@ def reschedule_meeting_for_application(request, slug):
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
@login_required
|
||||
def user_profile_image_update(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
@ -2916,7 +2977,7 @@ def user_profile_image_update(request, pk):
|
||||
}
|
||||
return render(request, "user/profile.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def user_detail(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
@ -2936,6 +2997,8 @@ def user_detail(request, pk):
|
||||
return render(request, "user/profile.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def easy_logs(request):
|
||||
"""
|
||||
Function-based view to display Django Easy Audit logs with tab switching and pagination.
|
||||
@ -2983,7 +3046,8 @@ def is_superuser_check(user):
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@login_required
|
||||
@superuser_required
|
||||
def create_staff_user(request):
|
||||
if request.method == "POST":
|
||||
form = StaffUserCreationForm(request.POST)
|
||||
@ -3000,13 +3064,15 @@ def create_staff_user(request):
|
||||
return render(request, "user/create_staff.html", {"form": form})
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@login_required
|
||||
@superuser_required
|
||||
def admin_settings(request):
|
||||
staffs = User.objects.filter(user_type="staff",is_superuser=False)
|
||||
form = ToggleAccountForm()
|
||||
context = {"staffs": staffs, "form": form}
|
||||
return render(request, "user/admin_settings.html", context)
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def staff_assignment_view(request, slug):
|
||||
"""
|
||||
@ -3040,8 +3106,8 @@ def staff_assignment_view(request, slug):
|
||||
|
||||
from django.contrib.auth.forms import SetPasswordForm
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@login_required
|
||||
@superuser_required
|
||||
def set_staff_password(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
print(request.POST)
|
||||
@ -3062,8 +3128,8 @@ def set_staff_password(request, pk):
|
||||
request, "user/staff_password_create.html", {"form": form, "user": user}
|
||||
)
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@login_required
|
||||
@superuser_required
|
||||
def account_toggle_status(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
if request.method == "POST":
|
||||
@ -3273,6 +3339,7 @@ def zoom_webhook_view(request):
|
||||
|
||||
|
||||
# Hiring Agency CRUD Views
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_list(request):
|
||||
"""List all hiring agencies with search and pagination"""
|
||||
@ -3303,6 +3370,8 @@ def agency_list(request):
|
||||
return render(request, "recruitment/agency_list.html", context)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_create(request):
|
||||
"""Create a new hiring agency"""
|
||||
@ -3325,6 +3394,31 @@ def agency_create(request):
|
||||
return render(request, "recruitment/agency_form.html", context)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def regenerate_agency_password(request, slug):
|
||||
agency=HiringAgency.objects.get(slug=slug)
|
||||
new_password=generate_random_password()
|
||||
agency.generated_password=new_password
|
||||
agency.save()
|
||||
user=agency.user
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
messages.success(request, f'New password generated for agency "{agency.name}" successfully!')
|
||||
return redirect("agency_detail", slug=agency.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def deactivate_agency(request, slug):
|
||||
agency = get_object_or_404(HiringAgency, slug=slug)
|
||||
agency.is_active = False
|
||||
agency.save()
|
||||
messages.success(request, f'Agency "{agency.name}" deactivated successfully!')
|
||||
return redirect("agency_detail", slug=agency.slug)
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_detail(request, slug):
|
||||
"""View details of a specific hiring agency"""
|
||||
@ -3360,7 +3454,7 @@ def agency_detail(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/agency_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_update(request, slug):
|
||||
"""Update an existing hiring agency"""
|
||||
@ -3385,7 +3479,7 @@ def agency_update(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/agency_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_delete(request, slug):
|
||||
"""Delete a hiring agency"""
|
||||
@ -3716,7 +3810,7 @@ def agency_delete(request, slug):
|
||||
# }
|
||||
# return render(request, 'recruitment/agency_candidates.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_applications(request, slug):
|
||||
"""View all applications from a specific agency"""
|
||||
@ -3748,6 +3842,7 @@ def agency_applications(request, slug):
|
||||
|
||||
|
||||
# Agency Portal Management Views
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_assignment_list(request):
|
||||
"""List all agency job assignments"""
|
||||
@ -3782,6 +3877,7 @@ def agency_assignment_list(request):
|
||||
return render(request, "recruitment/agency_assignment_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_assignment_create(request, slug=None):
|
||||
"""Create a new agency job assignment"""
|
||||
@ -3820,6 +3916,7 @@ def agency_assignment_create(request, slug=None):
|
||||
return render(request, "recruitment/agency_assignment_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_assignment_detail(request, slug):
|
||||
"""View details of a specific agency assignment"""
|
||||
@ -3856,7 +3953,7 @@ def agency_assignment_detail(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/agency_assignment_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_assignment_update(request, slug):
|
||||
"""Update an existing agency assignment"""
|
||||
@ -3881,7 +3978,7 @@ def agency_assignment_update(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/agency_assignment_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_access_link_create(request):
|
||||
"""Create access link for agency assignment"""
|
||||
@ -3909,6 +4006,7 @@ def agency_access_link_create(request):
|
||||
return render(request, "recruitment/agency_access_link_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_access_link_detail(request, slug):
|
||||
"""View details of an access link"""
|
||||
@ -3924,7 +4022,7 @@ def agency_access_link_detail(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/agency_access_link_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_assignment_extend_deadline(request, slug):
|
||||
"""Extend deadline for an agency assignment"""
|
||||
@ -4124,6 +4222,7 @@ def applicant_portal_dashboard(request):
|
||||
|
||||
|
||||
@login_required
|
||||
@candidate_user_required
|
||||
def applicant_application_detail(request, slug):
|
||||
"""View detailed information about a specific application"""
|
||||
if not request.user.is_authenticated:
|
||||
@ -4176,6 +4275,7 @@ def applicant_application_detail(request, slug):
|
||||
return render(request, "recruitment/applicant_application_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_portal_persons_list(request):
|
||||
"""Agency portal page showing all persons who come through this agency"""
|
||||
@ -4231,6 +4331,7 @@ def agency_portal_persons_list(request):
|
||||
return render(request, "recruitment/agency_portal_persons_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_portal_dashboard(request):
|
||||
"""Agency portal dashboard showing all assignments for the agency"""
|
||||
@ -4288,6 +4389,7 @@ def agency_portal_dashboard(request):
|
||||
return render(request, "recruitment/agency_portal_dashboard.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_portal_submit_application_page(request, slug):
|
||||
"""Dedicated page for submitting a application """
|
||||
@ -4354,7 +4456,7 @@ def agency_portal_submit_application_page(request, slug):
|
||||
}
|
||||
return render(request, "recruitment/agency_portal_submit_application.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_portal_submit_application(request):
|
||||
"""Handle candidate submission via AJAX (for embedded form)"""
|
||||
@ -4419,6 +4521,8 @@ def agency_portal_submit_application(request):
|
||||
return render(request, "recruitment/agency_portal_submit_application.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_or_agency_required
|
||||
def agency_portal_assignment_detail(request, slug):
|
||||
"""View details of a specific assignment - routes to admin or agency template"""
|
||||
assignment = get_object_or_404(
|
||||
@ -4439,6 +4543,7 @@ def agency_portal_assignment_detail(request, slug):
|
||||
return redirect("portal_login")
|
||||
|
||||
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_assignment_detail_agency(request, slug, assignment_id):
|
||||
"""Handle agency portal assignment detail view"""
|
||||
@ -4501,6 +4606,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
|
||||
return render(request, "recruitment/agency_portal_assignment_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_assignment_detail_admin(request, slug):
|
||||
"""Handle admin assignment detail view"""
|
||||
@ -4530,6 +4636,7 @@ def agency_assignment_detail_admin(request, slug):
|
||||
|
||||
|
||||
#will check the changes application to appliaction in this function
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_portal_edit_application(request, candidate_id):
|
||||
"""Edit a candidate for agency portal"""
|
||||
@ -4591,6 +4698,7 @@ def agency_portal_edit_application(request, candidate_id):
|
||||
return redirect("agency_portal_dashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
@agency_user_required
|
||||
def agency_portal_delete_application(request, candidate_id):
|
||||
"""Delete a candidate for agency portal"""
|
||||
@ -4629,7 +4737,7 @@ def agency_portal_delete_application(request, candidate_id):
|
||||
|
||||
|
||||
# Message Views
|
||||
|
||||
@login_required
|
||||
def message_list(request):
|
||||
"""List all messages for the current user"""
|
||||
# Get filter parameters
|
||||
@ -4721,10 +4829,14 @@ def message_create(request):
|
||||
# Send email if message_type is 'email' and recipient has email
|
||||
|
||||
if message.recipient and message.recipient.email:
|
||||
if request.user.user_type != "staff":
|
||||
message=message.content
|
||||
else:
|
||||
message=message.content.append(f"\n\n Sent by: {request.user.get_full_name()} ({request.user.email})")
|
||||
try:
|
||||
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
||||
subject=message.subject,
|
||||
body_message=message.content,
|
||||
body_message=message,
|
||||
recipient=message.recipient.email,
|
||||
attachments=None,
|
||||
sender=False,
|
||||
@ -5262,6 +5374,7 @@ def portal_logout(request):
|
||||
|
||||
|
||||
# Interview Creation Views
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def interview_create_type_selection(request, application_slug):
|
||||
"""Show interview type selection page for a application"""
|
||||
@ -5274,6 +5387,7 @@ def interview_create_type_selection(request, application_slug):
|
||||
return render(request, 'interviews/interview_create_type_selection.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def interview_create_remote(request, application_slug):
|
||||
"""Create remote interview for a candidate"""
|
||||
@ -5306,6 +5420,7 @@ def interview_create_remote(request, application_slug):
|
||||
return render(request, 'interviews/interview_create_remote.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def interview_create_onsite(request, application_slug):
|
||||
"""Create onsite interview for a candidate"""
|
||||
@ -5344,6 +5459,8 @@ def interview_create_onsite(request, application_slug):
|
||||
return render(request, 'interviews/interview_create_onsite.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def get_interview_list(request, job_slug):
|
||||
from .forms import ScheduledInterviewUpdateStatusForm
|
||||
application = Application.objects.get(slug=job_slug)
|
||||
@ -5351,6 +5468,9 @@ def get_interview_list(request, job_slug):
|
||||
interview_status_form = ScheduledInterviewUpdateStatusForm()
|
||||
return render(request, 'interviews/partials/interview_list.html', {'interviews': interviews, 'application': application,'interview_status_form':interview_status_form})
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
@require_POST
|
||||
def update_interview_status(request,slug):
|
||||
from .forms import ScheduledInterviewUpdateStatusForm
|
||||
@ -5364,22 +5484,59 @@ def update_interview_status(request,slug):
|
||||
messages.success(request, "Interview status updated successfully.")
|
||||
return redirect('interview_detail', slug=slug)
|
||||
|
||||
@require_POST
|
||||
def cancel_interview_for_application(request,slug):
|
||||
scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||
if request.method == 'POST':
|
||||
if scheduled_interview.interview_type == 'REMOTE':
|
||||
result = delete_zoom_meeting(scheduled_interview.interview.meeting_id)
|
||||
if result["status"] != "success":
|
||||
messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}")
|
||||
return redirect('interview_detail', slug=slug)
|
||||
# @require_POST
|
||||
# def cancel_interview_for_application(request,slug):
|
||||
# scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||
# if request.method == 'POST':
|
||||
# if scheduled_interview.interview_type == 'REMOTE':
|
||||
# result = delete_zoom_meeting(scheduled_interview.interview.meeting_id)
|
||||
# if result["status"] != "success":
|
||||
# messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}")
|
||||
# return redirect('interview_detail', slug=slug)
|
||||
|
||||
scheduled_interview.delete()
|
||||
messages.success(request, "Interview cancelled successfully.")
|
||||
return redirect('interview_list')
|
||||
# scheduled_interview.delete()
|
||||
# messages.success(request, "Interview cancelled successfully.")
|
||||
# return redirect('interview_list')
|
||||
@require_POST
|
||||
@login_required # Assuming this should be protected
|
||||
@staff_user_required # Assuming only staff can cancel
|
||||
def cancel_interview_for_application(request, slug):
|
||||
"""
|
||||
Handles POST request to cancel an interview, setting the status
|
||||
and saving the form data (likely a reason for cancellation).
|
||||
"""
|
||||
interview = get_object_or_404(Interview, slug=slug)
|
||||
scheduled_interview = get_object_or_404(ScheduledInterview, interview=interview)
|
||||
form = InterviewCancelForm(request.POST, instance=interview)
|
||||
|
||||
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
interview.status = interview.Status.CANCELLED
|
||||
scheduled_interview.status = scheduled_interview.InterviewStatus.CANCELLED
|
||||
scheduled_interview.save(update_fields=['status'])
|
||||
interview.save(update_fields=['status']) # Saves the new status
|
||||
|
||||
form.save() # Saves form data
|
||||
|
||||
|
||||
|
||||
messages.success(request, _("Interview cancelled successfully."))
|
||||
return redirect('interview_detail', slug=scheduled_interview.slug)
|
||||
else:
|
||||
|
||||
error_list = [f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()]
|
||||
error_message = _("Please correct the following errors: ") + " ".join(error_list)
|
||||
messages.error(request, error_message)
|
||||
|
||||
|
||||
return redirect('interview_detail', slug=scheduled_interview.slug)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_access_link_deactivate(request, slug):
|
||||
"""Deactivate an agency access link"""
|
||||
access_link = get_object_or_404(
|
||||
@ -5417,6 +5574,7 @@ def agency_access_link_deactivate(request, slug):
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def agency_access_link_reactivate(request, slug):
|
||||
"""Reactivate an agency access link"""
|
||||
access_link = get_object_or_404(
|
||||
@ -5453,6 +5611,7 @@ def agency_access_link_reactivate(request, slug):
|
||||
return render(request, "recruitment/agency_access_link_confirm.html", context)
|
||||
|
||||
|
||||
|
||||
@agency_user_required
|
||||
def api_application_detail(request, candidate_id):
|
||||
"""API endpoint to get candidate details for agency portal"""
|
||||
@ -5490,7 +5649,7 @@ def api_application_detail(request, candidate_id):
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "error": str(e)})
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def compose_application_email(request, job_slug):
|
||||
"""Compose email to participants about a candidate"""
|
||||
@ -5625,7 +5784,7 @@ def compose_application_email(request, job_slug):
|
||||
|
||||
else:
|
||||
# GET request - show the form
|
||||
form = CandidateEmailForm(job, candidates)
|
||||
form = CandidateEmailForm(job, candidates,request)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@ -5637,6 +5796,7 @@ def compose_application_email(request, job_slug):
|
||||
|
||||
|
||||
# Source CRUD Views
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_list(request):
|
||||
"""List all sources with search and pagination"""
|
||||
@ -5666,6 +5826,7 @@ def source_list(request):
|
||||
return render(request, "recruitment/source_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_create(request):
|
||||
"""Create a new source"""
|
||||
@ -5688,6 +5849,7 @@ def source_create(request):
|
||||
return render(request, "recruitment/source_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_detail(request, slug):
|
||||
"""View details of a specific source"""
|
||||
@ -5715,6 +5877,7 @@ def source_detail(request, slug):
|
||||
return render(request, "recruitment/source_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_update(request, slug):
|
||||
"""Update an existing source"""
|
||||
@ -5740,6 +5903,7 @@ def source_update(request, slug):
|
||||
return render(request, "recruitment/source_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_delete(request, slug):
|
||||
"""Delete a source"""
|
||||
@ -5761,6 +5925,7 @@ def source_delete(request, slug):
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_generate_keys(request, slug):
|
||||
"""Generate new API keys for a source"""
|
||||
source = get_object_or_404(Source, slug=slug)
|
||||
@ -5787,6 +5952,7 @@ def source_generate_keys(request, slug):
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def source_toggle_status(request, slug):
|
||||
"""Toggle active status of a source"""
|
||||
source = get_object_or_404(Source, slug=slug)
|
||||
@ -5868,6 +6034,7 @@ def application_signup(request, slug):
|
||||
|
||||
|
||||
# Interview Views
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def interview_list(request):
|
||||
"""List all interviews with filtering and pagination"""
|
||||
@ -5906,20 +6073,23 @@ def interview_list(request):
|
||||
}
|
||||
return render(request, 'interviews/interview_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def interview_detail(request, slug):
|
||||
"""View details of a specific interview"""
|
||||
from .forms import ScheduledInterviewUpdateStatusForm
|
||||
|
||||
interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||
|
||||
|
||||
reschedule_form = ScheduledInterviewForm()
|
||||
reschedule_form.initial['topic'] = interview.interview.topic
|
||||
meeting=interview.interview
|
||||
context = {
|
||||
'interview': interview,
|
||||
'reschedule_form':reschedule_form,
|
||||
'interview_status_form':ScheduledInterviewUpdateStatusForm()
|
||||
'interview_status_form':ScheduledInterviewUpdateStatusForm(),
|
||||
'cancel_form':InterviewCancelForm(instance=meeting),
|
||||
}
|
||||
return render(request, 'interviews/interview_detail.html', context)
|
||||
|
||||
@ -6603,6 +6773,8 @@ def interview_detail(request, slug):
|
||||
|
||||
# return redirect('meeting_details', slug=slug)
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def application_add_note(request, slug):
|
||||
from .models import Note
|
||||
from .forms import NoteForm
|
||||
@ -6631,6 +6803,8 @@ def application_add_note(request, slug):
|
||||
notes = Note.objects.filter(application=application).order_by('-created_at')
|
||||
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':application,'notes':notes,'url':url})
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def interview_add_note(request, slug):
|
||||
from .models import Note
|
||||
from .forms import NoteForm
|
||||
@ -6656,3 +6830,25 @@ def interview_add_note(request, slug):
|
||||
form.fields['author'].widget = HiddenInput()
|
||||
|
||||
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()})
|
||||
|
||||
|
||||
# @login_required
|
||||
# @staff_user_required
|
||||
# def archieve_application_bank_view(request):
|
||||
# """View to list all applications in the application bank"""
|
||||
# applications = Application.objects.filter(stage="Applied").se
|
||||
# lect_related('person', 'job_posting').all().order_by('-created_at')
|
||||
|
||||
# paginator = Paginator(applications, 20) # Show 20 applications per page
|
||||
# page_number = request.GET.get('page')
|
||||
# page_obj = paginator.get_page(page_number)
|
||||
|
||||
# context = {
|
||||
# 'page_obj': page_obj,
|
||||
# 'applications': applications,
|
||||
# }
|
||||
# return render(request, 'jobs/archieve_applications_bank.html', context)
|
||||
|
||||
|
||||
|
||||
# In your views.py (or where the application views are defined)
|
||||
|
||||
@ -228,6 +228,8 @@ class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def retry_scoring_view(request,slug):
|
||||
from django_q.tasks import async_task
|
||||
|
||||
@ -302,6 +304,10 @@ def application_update_stage(request, slug):
|
||||
application.save(update_fields=['stage'])
|
||||
messages.success(request,_("application Stage Updated"))
|
||||
return redirect("application_detail",slug=application.slug)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
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']
|
||||
},
|
||||
'hired': {
|
||||
'filter': {'offer_status': 'Accepted'},
|
||||
'filter': {'stage': 'Hired'},
|
||||
'fields': ['name', 'email', 'phone', 'created_at', 'offer_date', 'ai_score', 'years_experience', 'professional_category', 'join_date'],
|
||||
'headers': ['Name', 'Email', 'Phone', 'Application Date', 'Hire Date', 'Match Score', 'Years Experience', 'Professional Category', 'Join Date']
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import secrets
|
||||
import string
|
||||
from .models import Source, IntegrationLog
|
||||
from .forms import SourceForm, generate_api_key, generate_api_secret
|
||||
from .decorators import login_required, staff_user_required
|
||||
|
||||
class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
|
||||
"""List all sources"""
|
||||
@ -182,6 +183,8 @@ class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
|
||||
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def generate_api_keys_view(request, pk):
|
||||
"""Generate new API keys for a specific source"""
|
||||
if not request.user.is_staff:
|
||||
@ -228,6 +231,8 @@ def generate_api_keys_view(request, pk):
|
||||
|
||||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def toggle_source_status_view(request, pk):
|
||||
"""Toggle the active status of a source"""
|
||||
if not request.user.is_staff:
|
||||
@ -267,7 +272,7 @@ def toggle_source_status_view(request, pk):
|
||||
# 'is_active': source.is_active,
|
||||
# 'message': f'Source "{source.name}" {status_text} successfully'
|
||||
# })
|
||||
|
||||
@login_required
|
||||
def copy_to_clipboard_view(request):
|
||||
"""HTMX endpoint to copy text to clipboard"""
|
||||
if request.method == 'POST':
|
||||
|
||||
209
requirements.tx
Normal file
209
requirements.tx
Normal 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
|
||||
100
requirements.txt
100
requirements.txt
@ -1,10 +1,8 @@
|
||||
amqp==5.3.1
|
||||
annotated-types==0.7.0
|
||||
anthropic==0.63.0
|
||||
anyio==4.11.0
|
||||
appdirs==1.4.4
|
||||
arrow==1.3.0
|
||||
asgiref==3.9.2
|
||||
asgiref==3.10.0
|
||||
asteval==1.0.6
|
||||
astunparse==1.6.3
|
||||
attrs==25.3.0
|
||||
@ -13,8 +11,8 @@ bleach==6.2.0
|
||||
blessed==1.22.0
|
||||
blinker==1.9.0
|
||||
blis==1.3.0
|
||||
boto3==1.40.37
|
||||
botocore==1.40.37
|
||||
boto3==1.40.45
|
||||
botocore==1.40.45
|
||||
bw-migrations==0.2
|
||||
bw2data==4.5
|
||||
bw2parameters==1.1.0
|
||||
@ -22,7 +20,8 @@ bw_processing==1.0
|
||||
cached-property==2.0.1
|
||||
catalogue==2.0.10
|
||||
celery==5.5.3
|
||||
certifi==2025.8.3
|
||||
certifi==2025.10.5
|
||||
cffi==2.0.0
|
||||
channels==4.3.1
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.3
|
||||
@ -35,50 +34,49 @@ confection==0.1.5
|
||||
constructive_geometries==1.0
|
||||
country_converter==1.3.1
|
||||
crispy-bootstrap5==2025.6
|
||||
cryptography==46.0.2
|
||||
cymem==2.0.11
|
||||
dataflows-tabulator==1.54.3
|
||||
datapackage==1.15.4
|
||||
datastar-py==0.6.5
|
||||
deepdiff==7.0.1
|
||||
Deprecated==1.2.18
|
||||
distro==1.9.0
|
||||
Django==5.2.6
|
||||
django-allauth==65.11.2
|
||||
Django==5.2.7
|
||||
django-allauth==65.12.1
|
||||
django-ckeditor-5==0.2.18
|
||||
django-cors-headers==4.9.0
|
||||
django-countries==7.6.1
|
||||
django-crispy-forms==2.4
|
||||
django-easy-audit==1.3.7
|
||||
django-encrypted-model-fields==0.6.5
|
||||
django-extensions==4.1
|
||||
django-filter==25.1
|
||||
django-js-asset==3.1.2
|
||||
django-picklefield==3.3
|
||||
django-q2==1.8.0
|
||||
django-summernote==0.8.20.0
|
||||
django-template-partials==25.2
|
||||
django-unfold==0.66.0
|
||||
django-unfold==0.67.0
|
||||
django-widget-tweaks==1.5.0
|
||||
django_celery_results==2.6.0
|
||||
djangorestframework==3.16.1
|
||||
docopt==0.6.2
|
||||
dotenv==0.9.9
|
||||
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
|
||||
et_xmlfile==2.0.0
|
||||
Faker==37.8.0
|
||||
filelock==3.19.1
|
||||
flexcache==0.3
|
||||
flexparser==0.4
|
||||
fsspec==2025.9.0
|
||||
gpt-po-translator==1.3.2
|
||||
greenlet==3.2.4
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
hf-xet==1.1.10
|
||||
huggingface-hub==0.35.3
|
||||
idna==3.10
|
||||
ijson==3.4.0
|
||||
iniconfig==2.1.0
|
||||
isodate==0.7.2
|
||||
isort==5.13.2
|
||||
Jinja2==3.1.6
|
||||
jiter==0.11.1
|
||||
jmespath==1.0.1
|
||||
joblib==1.5.2
|
||||
jsonlines==4.0.0
|
||||
jsonpointer==3.0.0
|
||||
jsonschema==4.25.1
|
||||
@ -87,37 +85,52 @@ kombu==5.5.4
|
||||
langcodes==3.5.0
|
||||
language_data==1.3.0
|
||||
linear-tsv==1.1.0
|
||||
llvmlite==0.45.0
|
||||
llvmlite==0.45.1
|
||||
loguru==0.7.3
|
||||
lxml==6.0.2
|
||||
marisa-trie==1.3.1
|
||||
markdown-it-py==4.0.0
|
||||
MarkupSafe==3.0.2
|
||||
MarkupSafe==3.0.3
|
||||
matrix_utils==0.6.2
|
||||
mdurl==0.1.2
|
||||
morefs==0.2.2
|
||||
mpmath==1.3.0
|
||||
mrio-common-metadata==0.2.1
|
||||
murmurhash==1.0.13
|
||||
numba==0.62.0
|
||||
networkx==3.5
|
||||
numba==0.62.1
|
||||
numpy==2.3.3
|
||||
openai==1.99.9
|
||||
nvidia-cublas-cu12==12.8.4.1
|
||||
nvidia-cuda-cupti-cu12==12.8.90
|
||||
nvidia-cuda-nvrtc-cu12==12.8.93
|
||||
nvidia-cuda-runtime-cu12==12.8.90
|
||||
nvidia-cudnn-cu12==9.10.2.21
|
||||
nvidia-cufft-cu12==11.3.3.83
|
||||
nvidia-cufile-cu12==1.13.1.3
|
||||
nvidia-curand-cu12==10.3.9.90
|
||||
nvidia-cusolver-cu12==11.7.3.90
|
||||
nvidia-cusparse-cu12==12.5.8.93
|
||||
nvidia-cusparselt-cu12==0.7.1
|
||||
nvidia-nccl-cu12==2.27.3
|
||||
nvidia-nvjitlink-cu12==12.8.93
|
||||
nvidia-nvtx-cu12==12.8.90
|
||||
openpyxl==3.1.5
|
||||
ordered-set==4.1.0
|
||||
packaging==25.0
|
||||
pandas==2.3.2
|
||||
pandas==2.3.3
|
||||
pdfminer.six==20250506
|
||||
pdfplumber==0.11.7
|
||||
peewee==3.18.2
|
||||
pillow==11.3.0
|
||||
Pint==0.25
|
||||
platformdirs==4.4.0
|
||||
pluggy==1.6.0
|
||||
polib==1.2.0
|
||||
preshed==3.0.10
|
||||
prettytable==3.16.0
|
||||
prompt_toolkit==3.0.52
|
||||
psycopg2-binary==2.9.11
|
||||
pycountry==24.6.1
|
||||
pydantic==2.11.9
|
||||
pydantic-settings==2.10.1
|
||||
psycopg==3.2.11
|
||||
pycparser==2.23
|
||||
pydantic==2.11.10
|
||||
pydantic-settings==2.11.0
|
||||
pydantic_core==2.33.2
|
||||
pyecospold==4.0.0
|
||||
Pygments==2.19.2
|
||||
@ -125,35 +138,36 @@ PyJWT==2.10.1
|
||||
PyMuPDF==1.26.4
|
||||
pyparsing==3.2.5
|
||||
PyPDF2==3.0.1
|
||||
pypdfium2==4.30.0
|
||||
PyPrind==2.11.3
|
||||
pytest==8.3.4
|
||||
pytest-django==4.11.1
|
||||
pytesseract==0.3.13
|
||||
python-dateutil==2.9.0.post0
|
||||
python-docx==1.2.0
|
||||
python-dotenv==1.0.1
|
||||
python-dotenv==1.1.1
|
||||
python-json-logger==3.3.0
|
||||
pytz==2025.2
|
||||
pyxlsb==1.0.10
|
||||
PyYAML==6.0.2
|
||||
PyYAML==6.0.3
|
||||
randonneur==0.6.2
|
||||
randonneur_data==0.6
|
||||
randonneur_data==0.6.1
|
||||
RapidFuzz==3.14.1
|
||||
rdflib==7.2.1
|
||||
redis==3.5.3
|
||||
referencing==0.36.2
|
||||
requests==2.32.3
|
||||
responses==0.25.8
|
||||
regex==2025.9.18
|
||||
requests==2.32.5
|
||||
rfc3986==2.0.0
|
||||
rich==14.1.0
|
||||
rpds-py==0.27.1
|
||||
s3transfer==0.14.0
|
||||
safetensors==0.6.2
|
||||
scikit-learn==1.7.2
|
||||
scipy==1.16.2
|
||||
sentence-transformers==5.1.1
|
||||
setuptools==80.9.0
|
||||
setuptools-scm==8.1.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
smart_open==7.3.1
|
||||
sniffio==1.3.1
|
||||
snowflake-id==1.0.2
|
||||
spacy==3.8.7
|
||||
spacy-legacy==3.0.12
|
||||
@ -165,15 +179,19 @@ sqlparse==0.5.3
|
||||
srsly==2.5.1
|
||||
stats_arrays==0.7
|
||||
structlog==25.4.0
|
||||
sympy==1.14.0
|
||||
tableschema==1.21.0
|
||||
tenacity==9.0.0
|
||||
thinc==8.3.6
|
||||
tomli==2.2.1
|
||||
threadpoolctl==3.6.0
|
||||
tokenizers==0.22.1
|
||||
toolz==1.0.0
|
||||
torch==2.8.0
|
||||
tqdm==4.67.1
|
||||
transformers==4.57.0
|
||||
triton==3.4.0
|
||||
typer==0.19.2
|
||||
types-python-dateutil==2.9.0.20251008
|
||||
typing-inspection==0.4.1
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.2
|
||||
unicodecsv==0.14.1
|
||||
|
||||
@ -632,11 +632,15 @@
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (value && !/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
|
||||
state.fieldErrors[field.id] = 'Please select a valid date';
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
// Regex for YYYY-MM-DD (ISO standard for <input type="date"> output)
|
||||
const yyyyMmDdRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
|
||||
|
||||
if (value && !yyyyMmDdRegex.test(value)) {
|
||||
// You might want to update the error message based on the input type
|
||||
state.fieldErrors[field.id] = 'Please enter a valid date (e.g., YYYY-MM-DD).';
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1024,7 +1028,7 @@
|
||||
}
|
||||
else if (field.type === 'date') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'month';
|
||||
input.type = 'date';
|
||||
input.className = 'form-input';
|
||||
input.placeholder = field.placeholder || 'Select date';
|
||||
input.id = `field_${field.id}`;
|
||||
@ -1265,5 +1269,8 @@
|
||||
|
||||
// Start the application
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock content %}
|
||||
@ -116,6 +116,7 @@
|
||||
<i class="fas fa-envelope"></i>
|
||||
</a>
|
||||
</li> {% endcomment %}
|
||||
|
||||
<li class="nav-item me-2 d-none d-lg-block">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
@ -135,23 +136,16 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% comment %} <li class="nav-item me-2 d-none d-lg-block">
|
||||
<a class="nav-link text-white" href="{% url 'message_list' %}">
|
||||
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endcomment %}
|
||||
|
||||
|
||||
<li class="nav-item mx-3 d-none d-lg-block mt-2">
|
||||
<a href="{% url 'message_list' %}"
|
||||
class=" btn btn-sm btn-outline-warning position-relative">
|
||||
<a href="{% url 'message_list' %}" class="btn btn-sm btn-outline-warning position-relative">
|
||||
<i class="fas fa-envelope me-1"></i>
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
||||
{{ request.user.get_unread_message_count }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link p-0 border-0 bg-transparent dropdown-toggle"
|
||||
@ -160,7 +154,7 @@
|
||||
aria-expanded="false"
|
||||
aria-label="{% trans 'Toggle user menu' %}"
|
||||
data-bs-auto-close="outside"
|
||||
data-bs-offset="0, 16" {# Vertical offset remains 16px to prevent clipping #}
|
||||
data-bs-offset="0, 16"
|
||||
>
|
||||
{% if user.profile_image %}
|
||||
<img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
|
||||
@ -168,16 +162,17 @@
|
||||
title="{% trans 'Your account' %}">
|
||||
{% else %}
|
||||
<div class="profile-avatar" title="{% trans 'Your account' %}">
|
||||
{{ user.first_name }} {{ user.last_name }}
|
||||
{% if user.first_name %}
|
||||
{{ user.first_name|first|capfirst }} {{ user.last_name|first|capfirst }}
|
||||
{% else %}
|
||||
{{user.username|first|capfirst}}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
|
||||
style="min-width: 240px;"
|
||||
>
|
||||
<li class="px-4 py-3 ">
|
||||
<ul class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3" style="min-width: 240px;">
|
||||
<li class="px-4 py-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
|
||||
{% if user.profile_image %}
|
||||
@ -197,67 +192,96 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<div>
|
||||
<li class="nav-item me-3 dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal d-lg-none">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% 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>
|
||||
{% 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 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Staff Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
|
||||
{% comment %} <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li> {% endcomment %}
|
||||
<li 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>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% comment %} CORRECTED LINKEDIN BLOCK {% endcomment %}
|
||||
{% if not request.session.linkedin_authenticated %}
|
||||
<li>
|
||||
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
|
||||
<i class="fab fa-linkedin me-3 text-primary fs-5"></i>
|
||||
<span>{% trans "Connect LinkedIn" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% comment %} {% if not request.session.linkedin_authenticated %}
|
||||
<li>
|
||||
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
|
||||
<i class="fab fa-linkedin me-3 text-primary fs-5"></i>
|
||||
<span>{% trans "Connect LinkedIn" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="px-4 py-2 text-muted small">
|
||||
<i class="fab fa-linkedin text-primary me-2"></i>
|
||||
{% trans "LinkedIn Connected" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="px-4 py-2 text-muted small">
|
||||
<i class="fab fa-linkedin text-primary me-2"></i>
|
||||
{% trans "LinkedIn Connected" %}
|
||||
</li>
|
||||
{% endif %} {% endcomment %}
|
||||
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<li>
|
||||
<form method="post" action="{% url 'account_logout'%}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="dropdown-item py-2 px-4 d-flex align-items-center border-0 bg-transparent text-start"
|
||||
class="dropdown-item py-2 px-4 d-flex align-items-center border-0 bg-transparent text-start w-100"
|
||||
aria-label="{% trans 'Sign out' %}"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
|
||||
<i class="fas fa-sign-out-alt me-3 fs-5" style="color:red;"></i>
|
||||
<span style="color:red;">{% trans "Sign Out" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@ -1415,8 +1415,8 @@ const elements = {
|
||||
</label>
|
||||
`;
|
||||
|
||||
// Add field input based on type
|
||||
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') {
|
||||
|
||||
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' ||field.type === 'date') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'field-input';
|
||||
|
||||
@ -150,7 +150,7 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 pb-5 px-lg-5" style="background-color: var(--color-background-light);">
|
||||
<h1 class="h3 fw-bold dashboard-header mb-4 px-3">
|
||||
<i class="fas fa-shield-alt me-2" style="color: var(--color-primary);"></i>{% trans "System Audit Logs" %}
|
||||
<i class="fas fa-shield-alt me-2" style="color: var(--color-primary);"></i>{% trans "System Activity Logs" %}
|
||||
</h1>
|
||||
|
||||
<div class="alert summary-alert border-start border-5 p-3 mb-5 mx-3" role="alert">
|
||||
|
||||
@ -85,8 +85,8 @@
|
||||
.bg-confirmed { background-color: var(--kaauh-info) !important; color: white; }
|
||||
.bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; }
|
||||
.bg-completed { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-remote { background-color: #007bff !important; color: white; }
|
||||
.bg-onsite { background-color: #6f42c1 !important; color: white; }
|
||||
.bg-remote { background-color: #004a53 color: white; }
|
||||
.bg-onsite { background-color: #00636e !important; color: white; }
|
||||
|
||||
/* Timeline Styling */
|
||||
.timeline {
|
||||
@ -206,7 +206,6 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Header Section -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
@ -224,18 +223,18 @@
|
||||
<a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
|
||||
</a>
|
||||
{% if interview.status != 'cancelled' %}
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#statusModal">
|
||||
<i class="fas fa-redo-alt me-1"></i> {% trans "Update Interview status" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Left Column - Candidate & Interview Info -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Candidate Information Panel -->
|
||||
<div class="kaauh-card shadow-sm p-4 mb-4">
|
||||
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
|
||||
@ -276,7 +275,7 @@
|
||||
<p class="mb-2"><strong>{% trans "Department:" %}</strong> {{ interview.job.department }}</p>
|
||||
<p class="mb-2"><strong>{% trans "Applied Date:" %}</strong> {{ interview.application.created_at|date:"d-m-Y" }}</p>
|
||||
<p class="mb-0"><strong>{% trans "Current Stage:" %}</strong>
|
||||
<span class="badge stage-badge stage-{{ interview.application.stage|lower }}">
|
||||
<span class="badge stage-badge bg-primary-theme">
|
||||
{{ interview.application.stage }}
|
||||
</span>
|
||||
</p>
|
||||
@ -285,7 +284,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interview Details Panel -->
|
||||
<div class="kaauh-card shadow-sm p-4 mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
|
||||
@ -384,7 +382,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline/History Section -->
|
||||
<div class="kaauh-card shadow-sm p-4">
|
||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
|
||||
<i class="fas fa-history me-2"></i> {% trans "Interview Timeline" %}
|
||||
@ -395,13 +392,13 @@
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">{% trans "Interview Scheduled" %}</h6>
|
||||
<p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"d-m-Y" }} {{ interview.interview_time|date:"h:i A" }}</p>
|
||||
<p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"F j, Y" }} {{ interview.interview_time|date:"h:i A" }}</p>
|
||||
</div>
|
||||
<small class="text-muted">{{ interview.interview.created_at|date:"d-m-Y h:i A" }}</small>
|
||||
<small class="text-muted">{{ interview.interview.created_at|date:"F j,Y h:i A" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if interview.interview.status == 'CONFIRMED' %}
|
||||
{% if interview.status == 'confirmed' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-content">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@ -414,7 +411,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if interview.interview.status == 'COMPLETED' %}
|
||||
{% if interview.status == 'completed' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-content">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@ -427,15 +424,15 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if interview.interview.status == 'CANCELLED' %}
|
||||
{% if interview.status == 'cancelled' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-content">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">{% trans "Interview Cancelled" %}</h6>
|
||||
<p class="mb-0 text-muted">{% trans "Interview was cancelled" %}</p>
|
||||
<p class="mb-0 text-muted">{% trans "Interview was cancelled on: " %}{{interview.interview.cancelled_at|date:"F j, Y"}}</p>
|
||||
</div>
|
||||
<small class="text-muted">{% trans "Recently" %}</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -444,15 +441,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Participants & Actions -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Participants Panel -->
|
||||
<div class="kaauh-card shadow-sm p-4 mb-4">
|
||||
{% comment %} <div class="kaauh-card shadow-sm p-4 mb-4">
|
||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
|
||||
<i class="fas fa-users me-2"></i> {% trans "Participants" %}
|
||||
</h5>
|
||||
|
||||
<!-- Internal Participants -->
|
||||
{% if interview.participants.exists %}
|
||||
<h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6>
|
||||
{% for participant in interview.participants.all %}
|
||||
@ -468,7 +462,6 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- External Participants -->
|
||||
{% if interview.system_users.exists %}
|
||||
<h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6>
|
||||
{% for user in interview.system_users.all %}
|
||||
@ -496,49 +489,71 @@
|
||||
data-bs-target="#participantModal">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %}
|
||||
</button>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
<!-- Actions Panel -->
|
||||
<div class="kaauh-card shadow-sm p-4">
|
||||
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
|
||||
<i class="fas fa-cog me-2"></i> {% trans "Actions" %}
|
||||
</h5>
|
||||
|
||||
<div class="action-buttons">
|
||||
{% if interview.status != 'CANCELLED' and interview.status != 'COMPLETED' %}
|
||||
|
||||
{% if interview.status != 'cancelled' and interview.status != 'COMPLETED' %}
|
||||
{# 1. Interview is Active (SCHEDULED/CONFIRMED) #}
|
||||
<button type="button" class="btn btn-main-action btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#rescheduleModal">
|
||||
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-warning btn-sm"
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#cancelModal">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
|
||||
</button>
|
||||
|
||||
{% elif interview.status == 'cancelled' %}
|
||||
{# 2. Interview is CANCELLED (Promote Reschedule) #}
|
||||
<div class="alert alert-danger w-100 py-2 small" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "Interview is CANCELLED." %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info py-2 small" role="alert" style="border-left: 5px solid #00636e; background-color: #f0f8ff;">
|
||||
{# Use an icon for visual appeal #}
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>{% trans "Reason:" %}</strong>
|
||||
{{ interview.interview.cancelled_reason }}
|
||||
|
||||
</div>
|
||||
|
||||
{% elif interview.status == 'COMPLETED' %}
|
||||
{# 3. Interview is COMPLETED (Promote Result Update) #}
|
||||
<div class="alert alert-success w-100 py-2 small" role="alert">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
{% trans "Interview is COMPLETED. Update the final result." %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#resultModal"
|
||||
style="width: 100%;">
|
||||
<i class="fas fa-clipboard-check me-1"></i> {% trans "Update Result" %}
|
||||
</button>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
<hr class="w-100 mt-2 mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm w-100"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#emailModal">
|
||||
<i class="fas fa-envelope me-1"></i> {% trans "Send Email" %}
|
||||
</button>
|
||||
|
||||
{% if interview.status == 'COMPLETED' %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#resultModal">
|
||||
<i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Candidate Profile Modal -->
|
||||
<div class="modal fade modal-xl" id="candidateModal" tabindex="-1" aria-labelledby="candidateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -558,7 +573,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Participant Modal -->
|
||||
<div class="modal fade" id="participantModal" tabindex="-1" aria-labelledby="participantModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -574,14 +588,12 @@
|
||||
<div class="mb-3">
|
||||
<label for="internal_participants" class="form-label">{% trans "Internal Participants" %}</label>
|
||||
<select multiple class="form-select" id="internal_participants" name="participants">
|
||||
<!-- Options will be populated dynamically -->
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="external_participants" class="form-label">{% trans "External Participants" %}</label>
|
||||
<select multiple class="form-select" id="external_participants" name="system_users">
|
||||
<!-- Options will be populated dynamically -->
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add Participants" %}
|
||||
@ -592,7 +604,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Modal -->
|
||||
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -616,23 +627,23 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email_message" class="form-label">{% trans "Message" %}</label>
|
||||
<textarea class="form-control" id="email_message" name="message" rows="6">
|
||||
{% trans "Dear" %} {{ interview.application.name }},
|
||||
<textarea class="form-control" id="email_message" name="message" rows="6">
|
||||
{% trans "Dear" %} {{ interview.application.name }},
|
||||
|
||||
{% trans "Your interview details are as follows:" %}
|
||||
{% trans "Your interview details are as follows:" %}
|
||||
|
||||
{% trans "Date:" %} {{ interview.interview_date|date:"d-m-Y" }}
|
||||
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }}
|
||||
{% trans "Job:" %} {{ interview.job.title }}
|
||||
{% trans "Date:" %} {{ interview.interview_date|date:"d-m-Y" }}
|
||||
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }}
|
||||
{% trans "Job:" %} {{ interview.job.title }}
|
||||
|
||||
{% if interview.interview.location_type == 'Remote' %}
|
||||
{% trans "This is a remote interview. You will receive the meeting link separately." %}
|
||||
{% else %}
|
||||
{% trans "This is an onsite interview. Please arrive 10 minutes early." %}
|
||||
{% endif %}
|
||||
{% if interview.interview.location_type == 'Remote' %}
|
||||
{% trans "This is a remote interview. You will receive the meeting link separately." %}
|
||||
{% else %}
|
||||
{% trans "This is an onsite interview. Please arrive 10 minutes early." %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Best regards," %}
|
||||
{% trans "HR Team" %}
|
||||
{% trans "Best regards," %}
|
||||
{% trans "HR Team" %}
|
||||
</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
@ -644,7 +655,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reschedule Modal -->
|
||||
<div class="modal fade" id="rescheduleModal" tabindex="-1" aria-labelledby="rescheduleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -657,20 +667,9 @@
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'reschedule_meeting_for_application' interview.slug %}">
|
||||
{% csrf_token %}
|
||||
{{reschedule_form|crispy}}
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="topic" class="form-label">{% trans "topic" %}</label>
|
||||
<input type="text" class="form-control" id="topic" name="topic" value="{{interview.interview.topic}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="start_time" class="form-label">{% trans "Start Time" %}</label>
|
||||
<input type="datetime-local" class="form-control" id="start_time" name="start_time" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="duration" class="form-label">{% trans "Duration" %}</label>
|
||||
<input type="number" class="form-control" id="duration" name="duration" required>
|
||||
</div> {% endcomment %}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
{{ reschedule_form|crispy }}
|
||||
|
||||
<button type="submit" class="btn btn-main-action btn-sm mt-3">
|
||||
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -679,7 +678,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Modal -->
|
||||
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -690,19 +688,20 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'cancel_interview_for_application' interview.slug %}">
|
||||
<form method="post" action="{% url 'cancel_interview_for_application' interview.interview.slug %}">
|
||||
{% csrf_token %}
|
||||
{{ cancel_form|crispy }}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Are you sure you want to cancel this interview? This action cannot be undone." %}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
|
||||
</button>
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -710,7 +709,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Modal -->
|
||||
<div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -737,7 +735,7 @@
|
||||
<textarea class="form-control" id="result_notes" name="notes" rows="4"
|
||||
placeholder="{% trans 'Add interview feedback and notes' %}"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<button type="submit" class="btn btn-main-action btn-sm mt-2">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Update Result" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -746,7 +744,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Status Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card">
|
||||
@ -759,14 +756,14 @@
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'update_interview_status' interview.slug %}">
|
||||
{% csrf_token %}
|
||||
{{interview_status_form|crispy}}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
{{ interview_status_form|crispy }}
|
||||
<div class="d-flex gap-2 mt-3 justify-content-end">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -780,7 +777,7 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Clear modal content when hidden
|
||||
const modals = ['candidateModal', 'participantModal', 'emailModal', 'rescheduleModal', 'cancelModal', 'resultModal'];
|
||||
const modals = ['candidateModal', 'participantModal', 'emailModal', 'rescheduleModal', 'cancelModal', 'resultModal', 'statusModal'];
|
||||
|
||||
modals.forEach(modalId => {
|
||||
const modal = document.getElementById(modalId);
|
||||
@ -788,6 +785,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
modal.addEventListener('hidden.bs.modal', function () {
|
||||
const modalBody = modal.querySelector('.modal-body');
|
||||
if (modalBody && modalId === 'candidateModal') {
|
||||
// Reset content for AJAX-loaded Candidate Modal
|
||||
modalBody.innerHTML = `
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
@ -795,9 +793,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// Note: For forms using Django/Crispy, you might need extra JS
|
||||
// to explicitly reset form fields if the form is not reloading
|
||||
// when the modal is closed.
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -509,6 +509,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<a href="{% url 'regenerate_agency_password' agency.slug %}"
|
||||
class="btn btn-danger"
|
||||
title="{% trans 'This action resets the agency\'s current login password.' %}"
|
||||
onclick="return confirm('{% trans "Are you sure you want to regenerate the password for this agency? This cannot be undone." %}');"
|
||||
>
|
||||
<i class="fas fa-key me-2"></i>
|
||||
{% trans "Regenerate Agency Password" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
220
templates/user/settings.html
Normal file
220
templates/user/settings.html
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user