fix remote interview creation issue

This commit is contained in:
ismail 2025-12-04 15:02:43 +03:00
parent bfea43df8f
commit bb552cbd3f
28 changed files with 2069 additions and 477 deletions

6
.env
View File

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

View File

@ -273,6 +273,8 @@ SOCIALACCOUNT_PROVIDERS = {
} }
} }
# Dynamic Zoom Configuration - will be loaded from database
# These are fallback values - actual values will be loaded from database at runtime
ZOOM_ACCOUNT_ID = "HoGikHXsQB2GNDC5Rvyw9A" ZOOM_ACCOUNT_ID = "HoGikHXsQB2GNDC5Rvyw9A"
ZOOM_CLIENT_ID = "brC39920R8C8azfudUaQgA" ZOOM_CLIENT_ID = "brC39920R8C8azfudUaQgA"
ZOOM_CLIENT_SECRET = "rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L" ZOOM_CLIENT_SECRET = "rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L"
@ -292,6 +294,8 @@ CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json"
CELERY_TIMEZONE = "UTC" CELERY_TIMEZONE = "UTC"
# Dynamic LinkedIn Configuration - will be loaded from database
# These are fallback values - actual values will be loaded from database at runtime
LINKEDIN_CLIENT_ID = "867jwsiyem1504" LINKEDIN_CLIENT_ID = "867jwsiyem1504"
LINKEDIN_CLIENT_SECRET = "WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==" LINKEDIN_CLIENT_SECRET = "WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=="
LINKEDIN_REDIRECT_URI = "http://127.0.0.1:8000/jobs/linkedin/callback/" LINKEDIN_REDIRECT_URI = "http://127.0.0.1:8000/jobs/linkedin/callback/"

View File

@ -6,7 +6,7 @@ from .models import (
JobPosting, Application, TrainingMaterial, JobPosting, Application, TrainingMaterial,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note, SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview,Person AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview, Settings,Person
) )
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -249,6 +249,30 @@ admin.site.register(ScheduledInterview)
# AgencyMessage admin removed - model has been deleted # AgencyMessage admin removed - model has been deleted
@admin.register(Settings)
class SettingsAdmin(admin.ModelAdmin):
list_display = ['key', 'value_preview', 'created_at', 'updated_at']
list_filter = ['created_at']
search_fields = ['key', 'value']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Setting Information', {
'fields': ('key', 'value')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at')
}),
)
save_on_top = True
def value_preview(self, obj):
"""Show a preview of the value (truncated for long values)"""
if len(obj.value) > 50:
return obj.value[:50] + '...'
return obj.value
value_preview.short_description = 'Value'
admin.site.register(JobPostingImage) admin.site.register(JobPostingImage)
admin.site.register(Person) admin.site.register(Person)
# admin.site.register(User) # admin.site.register(User)

View File

@ -29,6 +29,7 @@ from .models import (
Person, Person,
Document, Document,
CustomUser, CustomUser,
Settings,
Interview Interview
) )
@ -2946,6 +2947,7 @@ class ScheduledInterviewUpdateStatusForm(forms.Form):
model = ScheduledInterview model = ScheduledInterview
fields = ['status'] fields = ['status']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -2958,3 +2960,78 @@ class ScheduledInterviewUpdateStatusForm(forms.Form):
# Apply the filtered list back to the field # Apply the filtered list back to the field
self.fields['status'].choices = filtered_choices self.fields['status'].choices = filtered_choices
class SettingsForm(forms.ModelForm):
"""Form for creating and editing settings"""
class Meta:
model = Settings
fields = ['key', 'value']
widgets = {
'key': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter setting key',
'required': True
}),
'value': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter setting value',
'required': True
}),
}
labels = {
'key': _('Setting Key'),
'value': _('Setting Value'),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'g-3'
self.helper.layout = Layout(
Field('key', css_class='form-control'),
Field('value', css_class='form-control'),
Div(
Submit('submit', _('Save Setting'), css_class='btn btn-main-action'),
css_class='col-12 mt-4',
),
)
def clean_key(self):
"""Ensure key is unique and properly formatted"""
key = self.cleaned_data.get('key')
if key:
# Convert to uppercase for consistency
key = key.upper().strip()
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Settings.objects.filter(key=key).exists():
raise forms.ValidationError("A setting with this key already exists.")
else: # Editing existing instance
if Settings.objects.filter(key=key).exclude(pk=instance.pk).exists():
raise forms.ValidationError("A setting with this key already exists.")
# Validate key format (alphanumeric and underscores only)
import re
if not re.match(r'^[A-Z][A-Z0-9_]*$', key):
raise forms.ValidationError(
"Setting key must start with a letter and contain only uppercase letters, numbers, and underscores."
)
return key
def clean_value(self):
"""Validate setting value"""
value = self.cleaned_data.get('value')
if value:
value = value.strip()
# You can add specific validation based on key type here
# For now, just ensure it's not empty
if not value:
raise forms.ValidationError("Setting value cannot be empty.")
return value

View File

@ -4,21 +4,22 @@ import uuid
import requests import requests
import logging import logging
import time import time
from django.conf import settings
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
from .utils import get_linkedin_config,get_setting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Define constants # Define constants
LINKEDIN_API_VERSION = '2.0.0' LINKEDIN_API_VERSION = get_setting('LINKEDIN_API_VERSION', '2.0.0')
LINKEDIN_VERSION = '202409' LINKEDIN_VERSION = get_setting('LINKEDIN_VERSION', '202301')
class LinkedInService: class LinkedInService:
def __init__(self): def __init__(self):
self.client_id = settings.LINKEDIN_CLIENT_ID config = get_linkedin_config()
self.client_secret = settings.LINKEDIN_CLIENT_SECRET self.client_id = config['LINKEDIN_CLIENT_ID']
self.redirect_uri = settings.LINKEDIN_REDIRECT_URI self.client_secret = config['LINKEDIN_CLIENT_SECRET']
self.redirect_uri = config['LINKEDIN_REDIRECT_URI']
self.access_token = None self.access_token = None
# Configuration for image processing wait time # Configuration for image processing wait time
self.ASSET_STATUS_TIMEOUT = 15 self.ASSET_STATUS_TIMEOUT = 15

View File

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from recruitment.utils import initialize_default_settings
class Command(BaseCommand):
help = 'Initialize Zoom and LinkedIn settings in the database from current hardcoded values'
def handle(self, *args, **options):
self.stdout.write('Initializing settings in database...')
try:
initialize_default_settings()
self.stdout.write(
self.style.SUCCESS('Successfully initialized settings in database')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Error initializing settings: {e}')
)

View File

@ -0,0 +1,30 @@
# Generated by Django 5.2.6 on 2025-12-03 17:52
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Settings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('key', models.CharField(help_text='Unique key for the setting', max_length=100, unique=True, verbose_name='Setting Key')),
('value', models.TextField(help_text='Value for the setting', verbose_name='Setting Value')),
],
options={
'verbose_name': 'Setting',
'verbose_name_plural': 'Settings',
'ordering': ['key'],
},
),
]

View File

@ -2739,3 +2739,24 @@ class Document(Base):
return "" return ""
class Settings(Base):
"""Model to store key-value pair settings"""
key = models.CharField(
max_length=100,
unique=True,
verbose_name=_("Setting Key"),
help_text=_("Unique key for the setting"),
)
value = models.TextField(
verbose_name=_("Setting Value"),
help_text=_("Value for the setting"),
)
class Meta:
verbose_name = _("Setting")
verbose_name_plural = _("Settings")
ordering = ["key"]
def __str__(self):
return f"{self.key}: {self.value[:50]}{'...' if len(self.value) > 50 else ''}"

View File

@ -44,31 +44,74 @@ def format_job(sender, instance, created, **kwargs):
# hook='myapp.tasks.email_sent_callback' # Optional callback # hook='myapp.tasks.email_sent_callback' # Optional callback
) )
else: # Enhanced reminder scheduling logic
existing_schedule = Schedule.objects.filter( if instance.status == "ACTIVE" and instance.application_deadline:
func="recruitment.tasks.form_close", # Schedule 1-day reminder
args=f"[{instance.pk}]", one_day_schedule = Schedule.objects.filter(
schedule_type=Schedule.ONCE, name=f"one_day_reminder_{instance.pk}"
).first() ).first()
if instance.STATUS_CHOICES == "ACTIVE" and instance.application_deadline: one_day_before = instance.application_deadline - timedelta(days=1)
if not existing_schedule: if not one_day_schedule:
# Create a new schedule if one does not exist
schedule( schedule(
"recruitment.tasks.form_close", "recruitment.tasks.send_one_day_reminder",
instance.pk,
schedule_type=Schedule.ONCE,
next_run=one_day_before,
repeats=-1,
name=f"one_day_reminder_{instance.pk}",
)
elif one_day_schedule.next_run != one_day_before:
one_day_schedule.next_run = one_day_before
one_day_schedule.save()
# Schedule 15-minute reminder
fifteen_min_schedule = Schedule.objects.filter(
name=f"fifteen_min_reminder_{instance.pk}"
).first()
fifteen_min_before = instance.application_deadline - timedelta(minutes=15)
if not fifteen_min_schedule:
schedule(
"recruitment.tasks.send_fifteen_minute_reminder",
instance.pk,
schedule_type=Schedule.ONCE,
next_run=fifteen_min_before,
repeats=-1,
name=f"fifteen_min_reminder_{instance.pk}",
)
elif fifteen_min_schedule.next_run != fifteen_min_before:
fifteen_min_schedule.next_run = fifteen_min_before
fifteen_min_schedule.save()
# Schedule job closing notification (enhanced form_close)
closing_schedule = Schedule.objects.filter(
name=f"job_closing_{instance.pk}"
).first()
if not closing_schedule:
schedule(
"recruitment.tasks.send_job_closed_notification",
instance.pk, instance.pk,
schedule_type=Schedule.ONCE, schedule_type=Schedule.ONCE,
next_run=instance.application_deadline, next_run=instance.application_deadline,
repeats=-1, # Ensure the schedule is deleted after it runs repeats=-1,
name=f"job_closing_{instance.pk}", # Add a name for easier lookup name=f"job_closing_{instance.pk}",
) )
elif existing_schedule.next_run != instance.application_deadline: elif closing_schedule.next_run != instance.application_deadline:
# Update an existing schedule's run time closing_schedule.next_run = instance.application_deadline
existing_schedule.next_run = instance.application_deadline closing_schedule.save()
existing_schedule.save()
elif existing_schedule: else:
# If the instance is no longer active, delete the scheduled task # Clean up all reminder schedules if job is no longer active
existing_schedule.delete() reminder_schedules = Schedule.objects.filter(
name__in=[f"one_day_reminder_{instance.pk}",
f"fifteen_min_reminder_{instance.pk}",
f"job_closing_{instance.pk}"]
)
if reminder_schedules.exists():
reminder_schedules.delete()
logger.info(f"Cleaned up reminder schedules for job {instance.pk}")
# @receiver(post_save, sender=JobPosting) # @receiver(post_save, sender=JobPosting)

View File

@ -14,6 +14,8 @@ from . models import JobPosting
from django.utils import timezone from django.utils import timezone
from . models import BulkInterviewTemplate,Interview,Message,ScheduledInterview from . models import BulkInterviewTemplate,Interview,Message,ScheduledInterview
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from .utils import get_setting
User = get_user_model() User = get_user_model()
# Add python-docx import for Word document processing # Add python-docx import for Word document processing
try: try:
@ -26,8 +28,11 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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' OPENROUTER_API_URL = get_setting('OPENROUTER_API_URL')
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct' OPENROUTER_API_KEY = get_setting('OPENROUTER_API_KEY')
OPENROUTER_MODEL = get_setting('OPENROUTER_MODEL')
# OPENROUTER_API_KEY ='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'
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct'
# OPENROUTER_MODEL = 'qwen/qwen-2.5-7b-instruct' # OPENROUTER_MODEL = 'qwen/qwen-2.5-7b-instruct'
# OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'openai/gpt-oss-20b'
@ -193,7 +198,7 @@ def format_job_description(pk):
def ai_handler(prompt): def ai_handler(prompt):
print("model call") print("model call")
response = requests.post( response = requests.post(
url="https://openrouter.ai/api/v1/chat/completions", url=OPENROUTER_API_URL,
headers={ headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}", "Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -668,54 +673,25 @@ def handle_resume_parsing_and_scoring(pk: int):
from django.utils import timezone from django.utils import timezone
def create_interview_and_meeting( def create_interview_and_meeting(schedule_id):
application_id,
job_id,
schedule_id,
slot_date,
slot_time,
duration
):
""" """
Synchronous task for a single interview slot, dispatched by django-q. Synchronous task for a single interview slot, dispatched by django-q.
""" """
try: try:
application = Application.objects.get(pk=application_id) schedule = ScheduledInterview.objects.get(pk=schedule_id)
job = JobPosting.objects.get(pk=job_id) interview = schedule.interview
schedule = BulkInterviewTemplate.objects.get(pk=schedule_id)
interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time)) result = create_zoom_meeting(interview.topic, interview.start_time, interview.duration)
meeting_topic = schedule.topic
result = create_zoom_meeting(meeting_topic, interview_datetime, duration)
if result["status"] == "success": if result["status"] == "success":
interview = Interview.objects.create( interview.meeting_id = result["meeting_details"]["meeting_id"]
topic=meeting_topic, interview.details_url = result["meeting_details"]["join_url"]
start_time=interview_datetime, interview.zoom_gateway_response = result["zoom_gateway_response"]
duration=duration, interview.host_email = result["meeting_details"]["host_email"]
meeting_id=result["meeting_details"]["meeting_id"], interview.password = result["meeting_details"]["password"]
details_url=result["meeting_details"]["join_url"], interview.save()
zoom_gateway_response=result["zoom_gateway_response"],
host_email=result["meeting_details"]["host_email"],
password=result["meeting_details"]["password"],
location_type="Remote"
)
schedule = ScheduledInterview.objects.create(
application=application,
job=job,
schedule=schedule,
interview_date=slot_date,
interview_time=slot_time,
interview=interview
)
schedule.interview = interview
schedule.status = "scheduled"
schedule.save()
logger.info(f"Successfully scheduled interview for {Application.name}") logger.info(f"Successfully scheduled interview for {Application.name}")
return True # Task succeeded return True
else: else:
# Handle Zoom API failure (e.g., log it or notify administrator) # Handle Zoom API failure (e.g., log it or notify administrator)
logger.error(f"Zoom API failed for {Application.name}: {result['message']}") logger.error(f"Zoom API failed for {Application.name}: {result['message']}")
@ -1122,3 +1098,202 @@ def generate_and_save_cv_zip(job_posting_id):
job.save() job.save()
return f"Successfully created zip for Job ID {job.slug} {job_posting_id}" return f"Successfully created zip for Job ID {job.slug} {job_posting_id}"
def send_one_day_reminder(job_id):
"""
Send email reminder 1 day before job application deadline.
"""
try:
job = JobPosting.objects.get(pk=job_id)
# Only send if job is still active
if job.status != 'ACTIVE':
logger.info(f"Job {job_id} is no longer active, skipping 1-day reminder")
return
# Get application count
application_count = Application.objects.filter(job=job).count()
# Determine recipients
recipients = []
if job.assigned_to:
recipients.append(job.assigned_to.email)
# Add admin users as fallback or additional recipients
admin_users = User.objects.filter(is_staff=True)
if not recipients: # If no assigned user, send to all admins
recipients = [admin.email for admin in admin_users]
if not recipients:
logger.warning(f"No recipients found for job {job_id} 1-day reminder")
return
# Create email content
subject = f"Reminder: Job '{job.title}' closes tomorrow"
html_message = f"""
<html>
<body>
<h2>Job Closing Reminder</h2>
<p><strong>Job Title:</strong> {job.title}</p>
<p><strong>Application Deadline:</strong> {job.application_deadline.strftime('%B %d, %Y')}</p>
<p><strong>Current Applications:</strong> {application_count}</p>
<p><strong>Status:</strong> {job.get_status_display()}</p>
<p>This job posting will close <strong>tomorrow</strong>. Please review any pending applications before the deadline.</p>
<p><a href="/recruitment/jobs/{job.pk}/" style="background-color: #007cba; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Job Details</a></p>
<hr>
<p><small>This is an automated reminder from the KAAUH Recruitment System.</small></p>
</body>
</html>
"""
# Send email to each recipient
for recipient_email in recipients:
_task_send_individual_email(subject, html_message, recipient_email, None, None, None)
logger.info(f"Sent 1-day reminder for job {job_id} to {len(recipients)} recipients")
except JobPosting.DoesNotExist:
logger.error(f"Job {job_id} not found for 1-day reminder")
except Exception as e:
logger.error(f"Error sending 1-day reminder for job {job_id}: {str(e)}")
def send_fifteen_minute_reminder(job_id):
"""
Send final email reminder 15 minutes before job application deadline.
"""
try:
job = JobPosting.objects.get(pk=job_id)
# Only send if job is still active
if job.status != 'ACTIVE':
logger.info(f"Job {job_id} is no longer active, skipping 15-minute reminder")
return
# Get application count
application_count = Application.objects.filter(job=job).count()
# Determine recipients
recipients = []
if job.assigned_to:
recipients.append(job.assigned_to.email)
# Add admin users as fallback or additional recipients
admin_users = User.objects.filter(is_staff=True)
if not recipients: # If no assigned user, send to all admins
recipients = [admin.email for admin in admin_users]
if not recipients:
logger.warning(f"No recipients found for job {job_id} 15-minute reminder")
return
# Create email content
subject = f"FINAL REMINDER: Job '{job.title}' closes in 15 minutes"
html_message = f"""
<html>
<body>
<h2 style="color: #d63384;"> FINAL REMINDER</h2>
<p><strong>Job Title:</strong> {job.title}</p>
<p><strong>Application Deadline:</strong> {job.application_deadline.strftime('%B %d, %Y at %I:%M %p')}</p>
<p><strong>Current Applications:</strong> {application_count}</p>
<p><strong>Status:</strong> {job.get_status_display()}</p>
<p style="color: #d63384; font-weight: bold;">This job posting will close in <strong>15 minutes</strong>. This is your final reminder to review any pending applications.</p>
<p><a href="/recruitment/jobs/{job.pk}/" style="background-color: #dc3545; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Job Details Now</a></p>
<hr>
<p><small>This is an automated final reminder from the KAAUH Recruitment System.</small></p>
</body>
</html>
"""
# Send email to each recipient
for recipient_email in recipients:
_task_send_individual_email(subject, html_message, recipient_email, None, None, None)
logger.info(f"Sent 15-minute reminder for job {job_id} to {len(recipients)} recipients")
except JobPosting.DoesNotExist:
logger.error(f"Job {job_id} not found for 15-minute reminder")
except Exception as e:
logger.error(f"Error sending 15-minute reminder for job {job_id}: {str(e)}")
def send_job_closed_notification(job_id):
"""
Send notification when job has closed and update job status.
"""
try:
job = JobPosting.objects.get(pk=job_id)
# Only proceed if job is currently active
if job.status != 'ACTIVE':
logger.info(f"Job {job_id} is already not active, skipping closed notification")
return
# Get final application count
application_count = Application.objects.filter(job=job).count()
# Update job status to closed
job.status = 'CLOSED'
job.save(update_fields=['status'])
# Also close the form template
if job.template_form:
job.template_form.is_active = False
job.template_form.save(update_fields=['is_active'])
# Determine recipients
recipients = []
if job.assigned_to:
recipients.append(job.assigned_to.email)
# Add admin users as fallback or additional recipients
admin_users = User.objects.filter(is_staff=True)
if not recipients: # If no assigned user, send to all admins
recipients = [admin.email for admin in admin_users]
if not recipients:
logger.warning(f"No recipients found for job {job_id} closed notification")
return
# Create email content
subject = f"Job '{job.title}' has closed - {application_count} applications received"
html_message = f"""
<html>
<body>
<h2>Job Closed Notification</h2>
<p><strong>Job Title:</strong> {job.title}</p>
<p><strong>Application Deadline:</strong> {job.application_deadline.strftime('%B %d, %Y at %I:%M %p')}</p>
<p><strong>Total Applications Received:</strong> <strong style="color: #28a745;">{application_count}</strong></p>
<p><strong>Status:</strong> {job.get_status_display()}</p>
<p>The job posting has been automatically closed and is no longer accepting applications.</p>
<p><a href="/recruitment/jobs/{job.pk}/" style="background-color: #6c757d; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Job Details</a></p>
<p><a href="/recruitment/applications/?job={job.pk}" style="background-color: #007cba; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Applications</a></p>
<hr>
<p><small>This is an automated notification from the KAAUH Recruitment System.</small></p>
</body>
</html>
"""
# Send email to each recipient
for recipient_email in recipients:
_task_send_individual_email(subject, html_message, recipient_email, None, None, None)
logger.info(f"Sent job closed notification for job {job_id} to {len(recipients)} recipients")
except JobPosting.DoesNotExist:
logger.error(f"Job {job_id} not found for closed notification")
except Exception as e:
logger.error(f"Error sending job closed notification for job {job_id}: {str(e)}")

View File

@ -264,11 +264,11 @@ urlpatterns = [
# path('api/templates/save/', views.save_form_template, name='save_form_template'), # path('api/templates/save/', views.save_form_template, name='save_form_template'),
# path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'), # path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
# path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'), # path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
# path( path(
# "jobs/<slug:slug>/calendar/", "jobs/<slug:slug>/calendar/",
# views.interview_calendar_view, views.interview_calendar_view,
# name="interview_calendar", name="interview_calendar",
# ), ),
# path( # path(
# "jobs/<slug:slug>/calendar/interview/<int:interview_id>/", # "jobs/<slug:slug>/calendar/interview/<int:interview_id>/",
# views.interview_detail_view, # views.interview_detail_view,
@ -283,8 +283,13 @@ urlpatterns = [
name="user_profile_image_update", name="user_profile_image_update",
), ),
path("easy_logs/", views.easy_logs, name="easy_logs"), path("easy_logs/", views.easy_logs, name="easy_logs"),
path('settings/',views.settings,name="settings"), path("settings/", views.admin_settings, name="admin_settings"),
path("settings/admin/", views.admin_settings, name="admin_settings"), path("settings/list/", views.settings_list, name="settings_list"),
path("settings/create/", views.settings_create, name="settings_create"),
path("settings/<int:pk>/", views.settings_detail, name="settings_detail"),
path("settings/<int:pk>/update/", views.settings_update, name="settings_update"),
path("settings/<int:pk>/delete/", views.settings_delete, name="settings_delete"),
path("settings/<int:pk>/toggle/", views.settings_toggle_status, name="settings_toggle_status"),
path("staff/create", views.create_staff_user, name="create_staff_user"), path("staff/create", views.create_staff_user, name="create_staff_user"),
path( path(
"set_staff_password/<int:pk>/", "set_staff_password/<int:pk>/",

View File

@ -1,7 +1,6 @@
# import os """
# import fitz # PyMuPDF Utility functions for recruitment app
# import spacy """
# import requests
from recruitment import models from recruitment import models
from django.conf import settings from django.conf import settings
from datetime import datetime, timedelta, time, date from datetime import datetime, timedelta, time, date
@ -9,41 +8,282 @@ from django.utils import timezone
from .models import ScheduledInterview from .models import ScheduledInterview
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.mail import send_mail from django.core.mail import send_mail
import random
# nlp = spacy.load("en_core_web_sm")
# def extract_text_from_pdf(pdf_path):
# text = ""
# with fitz.open(pdf_path) as doc:
# for page in doc:
# text += page.get_text()
# return text
# def extract_summary_from_pdf(pdf_path):
# if not os.path.exists(pdf_path):
# return {'error': 'File not found'}
# text = extract_text_from_pdf(pdf_path)
# doc = nlp(text)
# summary = {
# 'name': doc.ents[0].text if doc.ents else '',
# 'skills': [chunk.text for chunk in doc.noun_chunks if len(chunk.text.split()) > 1],
# 'summary': text[:500]
# }
# return summary
import requests
from PyPDF2 import PdfReader
import os import os
import json import json
import logging import logging
import requests
from PyPDF2 import PdfReader
from django.conf import settings
from .models import Settings, Application
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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' def get_setting(key, default=None):
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' """
Get a setting value from the database, with fallback to environment variables and default
Args:
key (str): The setting key to retrieve
default: Default value if not found in database or environment
Returns:
The setting value from database, environment variable, or default
"""
try:
# First try to get from database
setting = Settings.objects.get(key=key)
return setting.value
except Settings.DoesNotExist:
# Fall back to environment variable
env_value = os.getenv(key)
if env_value is not None:
return env_value
# Finally return the default
return default
except Exception:
# In case of any database error, fall back to environment or default
env_value = os.getenv(key)
if env_value is not None:
return env_value
return default
def set_setting(key, value):
"""
Set a setting value in the database
Args:
key (str): The setting key
value: The setting value
Returns:
Settings: The created or updated setting object
"""
setting, created = Settings.objects.update_or_create(
key=key,
defaults={'value': str(value)}
)
return setting
def get_zoom_config():
"""
Get all Zoom configuration settings
Returns:
dict: Dictionary containing all Zoom settings
"""
return {
'ZOOM_ACCOUNT_ID': get_setting('ZOOM_ACCOUNT_ID'),
'ZOOM_CLIENT_ID': get_setting('ZOOM_CLIENT_ID'),
'ZOOM_CLIENT_SECRET': get_setting('ZOOM_CLIENT_SECRET'),
'ZOOM_WEBHOOK_API_KEY': get_setting('ZOOM_WEBHOOK_API_KEY'),
'SECRET_TOKEN': get_setting('SECRET_TOKEN'),
}
def get_linkedin_config():
"""
Get all LinkedIn configuration settings
Returns:
dict: Dictionary containing all LinkedIn settings
"""
return {
'LINKEDIN_CLIENT_ID': get_setting('LINKEDIN_CLIENT_ID'),
'LINKEDIN_CLIENT_SECRET': get_setting('LINKEDIN_CLIENT_SECRET'),
'LINKEDIN_REDIRECT_URI': get_setting('LINKEDIN_REDIRECT_URI'),
}
def get_applications_from_request(request):
"""
Extract application IDs from request and return Application objects
"""
application_ids = request.POST.getlist("candidate_ids")
if application_ids:
return Application.objects.filter(id__in=application_ids)
return Application.objects.none()
def schedule_interviews(schedule, applications):
"""
Schedule interviews for multiple applications based on a schedule template
"""
from .models import ScheduledInterview
from datetime import datetime, timedelta
scheduled_interviews = []
available_slots = get_available_time_slots(schedule)
for i, application in enumerate(applications):
if i < len(available_slots):
slot = available_slots[i]
interview = ScheduledInterview.objects.create(
application=application,
job=schedule.job,
interview_date=slot['date'],
interview_time=slot['time'],
status='scheduled'
)
scheduled_interviews.append(interview)
return scheduled_interviews
def get_available_time_slots(schedule):
"""
Calculate available time slots for interviews based on schedule
"""
from datetime import datetime, timedelta, time
import calendar
slots = []
current_date = schedule.start_date
while current_date <= schedule.end_date:
# Check if current date is a working day
weekday = current_date.weekday()
if str(weekday) in schedule.working_days:
# Calculate slots for this day
day_slots = _calculate_day_slots(schedule, current_date)
slots.extend(day_slots)
current_date += timedelta(days=1)
return slots
def _calculate_day_slots(schedule, date):
"""
Calculate available slots for a specific day
"""
from datetime import datetime, timedelta, time
slots = []
current_time = schedule.start_time
end_time = schedule.end_time
# Convert to datetime for easier calculation
current_datetime = datetime.combine(date, current_time)
end_datetime = datetime.combine(date, end_time)
# Calculate break times
break_start = None
break_end = None
if schedule.break_start_time and schedule.break_end_time:
break_start = datetime.combine(date, schedule.break_start_time)
break_end = datetime.combine(date, schedule.break_end_time)
while current_datetime + timedelta(minutes=schedule.interview_duration) <= end_datetime:
# Skip break time
if break_start and break_end:
if break_start <= current_datetime < break_end:
current_datetime = break_end
continue
slots.append({
'date': date,
'time': current_datetime.time()
})
# Move to next slot
current_datetime += timedelta(minutes=schedule.interview_duration + schedule.buffer_time)
return slots
def json_to_markdown_table(data):
"""
Convert JSON data to markdown table format
"""
if not data:
return ""
if isinstance(data, list):
if not data:
return ""
# Get headers from first item
first_item = data[0]
if isinstance(first_item, dict):
headers = list(first_item.keys())
rows = []
for item in data:
row = []
for header in headers:
value = item.get(header, '')
if isinstance(value, (dict, list)):
value = str(value)
row.append(str(value))
rows.append(row)
else:
# Simple list
headers = ['Value']
rows = [[str(item)] for item in data]
elif isinstance(data, dict):
headers = ['Key', 'Value']
rows = []
for key, value in data.items():
if isinstance(value, (dict, list)):
value = str(value)
rows.append([str(key), str(value)])
else:
# Single value
return str(data)
# Build markdown table
if not headers or not rows:
return str(data)
# Header row
table = "| " + " | ".join(headers) + " |\n"
# Separator row
table += "| " + " | ".join(["---"] * len(headers)) + " |\n"
# Data rows
for row in rows:
# Escape pipe characters in cells
escaped_row = [cell.replace("|", "\\|") for cell in row]
table += "| " + " | ".join(escaped_row) + " |\n"
return table
def initialize_default_settings():
"""
Initialize default settings in the database from current hardcoded values
This should be run once to migrate existing settings
"""
# Zoom settings
zoom_settings = {
'ZOOM_ACCOUNT_ID': getattr(settings, 'ZOOM_ACCOUNT_ID', ''),
'ZOOM_CLIENT_ID': getattr(settings, 'ZOOM_CLIENT_ID', ''),
'ZOOM_CLIENT_SECRET': getattr(settings, 'ZOOM_CLIENT_SECRET', ''),
'ZOOM_WEBHOOK_API_KEY': getattr(settings, 'ZOOM_WEBHOOK_API_KEY', ''),
'SECRET_TOKEN': getattr(settings, 'SECRET_TOKEN', ''),
}
# LinkedIn settings
linkedin_settings = {
'LINKEDIN_CLIENT_ID': getattr(settings, 'LINKEDIN_CLIENT_ID', ''),
'LINKEDIN_CLIENT_SECRET': getattr(settings, 'LINKEDIN_CLIENT_SECRET', ''),
'LINKEDIN_REDIRECT_URI': getattr(settings, 'LINKEDIN_REDIRECT_URI', ''),
}
# Create settings if they don't exist
all_settings = {**zoom_settings, **linkedin_settings}
for key, value in all_settings.items():
if value: # Only set if value exists
set_setting(key, value)
#####################################
if not OPENROUTER_API_KEY:
logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.")
def extract_text_from_pdf(file_path): def extract_text_from_pdf(file_path):
print("text extraction") print("text extraction")
@ -60,8 +300,12 @@ def extract_text_from_pdf(file_path):
def score_resume_with_openrouter(prompt): def score_resume_with_openrouter(prompt):
print("model call") print("model call")
OPENROUTER_API_URL = get_setting('OPENROUTER_API_URL')
OPENROUTER_API_KEY = get_setting('OPENROUTER_API_KEY')
OPENROUTER_MODEL = get_setting('OPENROUTER_MODEL')
response = requests.post( response = requests.post(
url="https://openrouter.ai/api/v1/chat/completions", url=OPENROUTER_API_URL,
headers={ headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}", "Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -75,7 +319,6 @@ def score_resume_with_openrouter(prompt):
# print(response.status_code) # print(response.status_code)
# print(response.json()) # print(response.json())
res = {} res = {}
print(response.status_code)
if response.status_code == 200: if response.status_code == 200:
res = response.json() res = response.json()
content = res["choices"][0]['message']['content'] content = res["choices"][0]['message']['content']
@ -123,21 +366,22 @@ def dashboard_callback(request, context):
def get_access_token(): def get_access_token():
"""Obtain an access token using server-to-server OAuth.""" """Obtain an access token using server-to-server OAuth."""
client_id = settings.ZOOM_CLIENT_ID ZOOM_ACCOUNT_ID = get_setting("ZOOM_ACCOUNT_ID")
client_secret = settings.ZOOM_CLIENT_SECRET ZOOM_CLIENT_ID = get_setting("ZOOM_CLIENT_ID")
ZOOM_CLIENT_SECRET = get_setting("ZOOM_CLIENT_SECRET")
ZOOM_AUTH_URL = get_setting("ZOOM_AUTH_URL")
auth_url = "https://zoom.us/oauth/token"
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
} }
data = { data = {
"grant_type": "account_credentials", "grant_type": "account_credentials",
"account_id": settings.ZOOM_ACCOUNT_ID, "account_id": ZOOM_ACCOUNT_ID,
} }
auth = (client_id, client_secret) auth = (ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET)
response = requests.post(auth_url, headers=headers, data=data, auth=auth) response = requests.post(ZOOM_AUTH_URL, headers=headers, data=data, auth=auth)
if response.status_code == 200: if response.status_code == 200:
return response.json().get("access_token") return response.json().get("access_token")
@ -181,8 +425,9 @@ def create_zoom_meeting(topic, start_time, duration):
"Authorization": f"Bearer {access_token}", "Authorization": f"Bearer {access_token}",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
ZOOM_MEETING_URL = get_setting('ZOOM_MEETING_URL')
response = requests.post( response = requests.post(
"https://api.zoom.us/v2/users/me/meetings", ZOOM_MEETING_URL,
headers=headers, headers=headers,
json=meeting_details json=meeting_details
) )
@ -585,19 +830,15 @@ def get_applications_from_request(request):
def update_meeting(instance, updated_data): def update_meeting(instance, updated_data):
result = update_zoom_meeting(instance.meeting_id, updated_data) result = update_zoom_meeting(instance.meeting_id, updated_data)
if result["status"] == "success": if result["status"] == "success":
# Fetch the latest details from Zoom after successful update
details_result = get_zoom_meeting_details(instance.meeting_id) details_result = get_zoom_meeting_details(instance.meeting_id)
if details_result["status"] == "success": if details_result["status"] == "success":
zoom_details = details_result["meeting_details"] zoom_details = details_result["meeting_details"]
# Update instance with fetched details
instance.topic = zoom_details.get("topic", instance.topic) instance.topic = zoom_details.get("topic", instance.topic)
instance.duration = zoom_details.get("duration", instance.duration) instance.duration = zoom_details.get("duration", instance.duration)
# instance.details_url = zoom_details.get("join_url", instance.details_url) instance.details_url = zoom_details.get("join_url", instance.details_url)
instance.password = zoom_details.get("password", instance.password) instance.password = zoom_details.get("password", instance.password)
# Corrected status assignment: instance.status, not instance.password
instance.status = zoom_details.get("status") instance.status = zoom_details.get("status")
instance.zoom_gateway_response = details_result.get("meeting_details") # Store full response instance.zoom_gateway_response = details_result.get("meeting_details") # Store full response

View File

@ -40,6 +40,7 @@ from .forms import (
RemoteInterviewForm, RemoteInterviewForm,
OnsiteInterviewForm, OnsiteInterviewForm,
BulkInterviewTemplateForm, BulkInterviewTemplateForm,
SettingsForm,
InterviewCancelForm InterviewCancelForm
) )
from .utils import generate_random_password from .utils import generate_random_password
@ -107,15 +108,16 @@ from django.views.generic import (
DeleteView, DeleteView,
) )
from .utils import ( from .utils import (
create_zoom_meeting,
delete_zoom_meeting,
get_applications_from_request, get_applications_from_request,
update_meeting,
update_zoom_meeting,
get_zoom_meeting_details,
schedule_interviews, schedule_interviews,
get_available_time_slots, get_available_time_slots,
) )
from .zoom_api import (
create_zoom_meeting,
delete_zoom_meeting,
update_zoom_meeting,
get_zoom_meeting_details,
)
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from .models import ( from .models import (
@ -141,7 +143,8 @@ from .models import (
Message, Message,
Document, Document,
Interview, Interview,
BulkInterviewTemplate BulkInterviewTemplate,
Settings
) )
@ -2103,7 +2106,8 @@ def applications_document_review_view(request, slug):
@require_POST @require_POST
@staff_user_required @staff_user_required
def reschedule_meeting_for_application(request, slug): def reschedule_meeting_for_application(request, slug):
schedule_interview = get_object_or_404(ScheduledInterview, slug=slug) from .utils import update_meeting
schedule = get_object_or_404(ScheduledInterview, slug=slug)
if request.method == "POST": if request.method == "POST":
form = ScheduledInterviewForm(request.POST) form = ScheduledInterviewForm(request.POST)
if form.is_valid(): if form.is_valid():
@ -2115,7 +2119,7 @@ def reschedule_meeting_for_application(request, slug):
"start_time": start_time.isoformat() + "Z", "start_time": start_time.isoformat() + "Z",
"duration": duration, "duration": duration,
} }
result = update_meeting(schedule_interview.interview, updated_data) result = update_meeting(schedule.interview, updated_data)
if result["status"] == "success": if result["status"] == "success":
messages.success(request, result["message"]) messages.success(request, result["message"])
@ -2124,7 +2128,7 @@ def reschedule_meeting_for_application(request, slug):
else: else:
print(form.errors) print(form.errors)
messages.error(request, "Invalid data submitted.") messages.error(request, "Invalid data submitted.")
return redirect("interview_detail", slug=schedule_interview.slug) return redirect("interview_detail", slug=schedule.slug)
# context = {"job": job, "application": application, "meeting": meeting, "form": form} # context = {"job": job, "application": application, "meeting": meeting, "form": form}
# return render(request, "meetings/reschedule_meeting.html", context) # return render(request, "meetings/reschedule_meeting.html", context)
@ -4746,6 +4750,7 @@ def message_list(request):
status_filter = request.GET.get("status", "") status_filter = request.GET.get("status", "")
message_type_filter = request.GET.get("type", "") message_type_filter = request.GET.get("type", "")
search_query = request.GET.get("q", "") search_query = request.GET.get("q", "")
job_filter = request.GET.get("job_filter", "")
# Base queryset - get messages where user is either sender or recipient # Base queryset - get messages where user is either sender or recipient
message_list = ( message_list = (
@ -4754,16 +4759,20 @@ def message_list(request):
.order_by("-created_at") .order_by("-created_at")
) )
jobs = JobPosting.objects.all()
# Apply filters # Apply filters
if status_filter: if status_filter:
if status_filter == "read": if status_filter == "read":
message_list = message_list.filter(is_read=True) message_list = message_list.filter(is_read=True)
elif status_filter == "unread": elif status_filter == "unread":
message_list = message_list.filter(is_read=False) message_list = message_list.filter(is_read=False)
if message_type_filter: if message_type_filter:
message_list = message_list.filter(message_type=message_type_filter) message_list = message_list.filter(message_type=message_type_filter)
if request.user.user_type == "staff" and job_filter:
job = get_object_or_404(JobPosting, pk=job_filter)
message_list = message_list.filter(job=job)
if search_query: if search_query:
message_list = message_list.filter( message_list = message_list.filter(
Q(subject__icontains=search_query) | Q(content__icontains=search_query) Q(subject__icontains=search_query) | Q(content__icontains=search_query)
@ -4785,6 +4794,8 @@ def message_list(request):
"status_filter": status_filter, "status_filter": status_filter,
"type_filter": message_type_filter, "type_filter": message_type_filter,
"search_query": search_query, "search_query": search_query,
"job_filter": job_filter,
"jobs": jobs,
} }
if request.user.user_type != "staff": if request.user.user_type != "staff":
return render(request, "messages/application_message_list.html", context) return render(request, "messages/application_message_list.html", context)
@ -4813,7 +4824,7 @@ def message_detail(request, message_id):
"message": message, "message": message,
} }
if request.user.user_type != "staff": if request.user.user_type != "staff":
return render(request, "messages/candidate_message_detail.html", context) return render(request, "messages/application_message_detail.html", context)
return render(request, "messages/message_detail.html", context) return render(request, "messages/message_detail.html", context)
@ -5400,15 +5411,15 @@ def interview_create_remote(request, application_slug):
if form.is_valid(): if form.is_valid():
try: try:
with transaction.atomic(): with transaction.atomic():
# Create ScheduledInterview record
schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"])
async_task( start_time = timezone.make_aware(datetime.combine(schedule.interview_date, schedule.interview_time))
"recruitment.tasks.create_interview_and_meeting", interview = Interview.objects.create(topic=form.cleaned_data["topic"],location_type="Remote",start_time=start_time,duration=form.cleaned_data['duration'])
application.pk, application.job.pk, schedule.pk, schedule.interview_date,schedule.interview_time, form.cleaned_data['duration'] schedule.interview = interview
) schedule.save()
async_task("recruitment.tasks.create_interview_and_meeting",schedule.pk)
messages.success(request, f"Remote interview scheduled for {application.name}") messages.success(request, f"Remote interview scheduled for {application.name}")
return redirect('interview_detail', slug=schedule.slug) return redirect('applications_interview_view', slug=application_slug)
except Exception as e: except Exception as e:
messages.error(request, f"Error creating remote interview: {str(e)}") messages.error(request, f"Error creating remote interview: {str(e)}")
@ -5443,7 +5454,7 @@ def interview_create_onsite(request, application_slug):
schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"])
messages.success(request, f"Onsite interview scheduled for {application.name}") messages.success(request, f"Onsite interview scheduled for {application.name}")
return redirect('interview_detail', slug=schedule.slug) return redirect('applications_interview_view', slug=application_slug)
except Exception as e: except Exception as e:
messages.error(request, f"Error creating onsite interview: {str(e)}") messages.error(request, f"Error creating onsite interview: {str(e)}")
@ -5723,7 +5734,7 @@ def compose_application_email(request, job_slug):
subject=subject, subject=subject,
content=message, content=message,
job=job, job=job,
message_type='email', message_type='job_related',
is_email_sent=True, is_email_sent=True,
email_address=application.person.email if application.person.email else application.email email_address=application.person.email if application.person.email else application.email
) )
@ -6084,13 +6095,16 @@ def interview_detail(request, slug):
"""View details of a specific interview""" """View details of a specific interview"""
from .forms import ScheduledInterviewUpdateStatusForm from .forms import ScheduledInterviewUpdateStatusForm
interview = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview
reschedule_form = ScheduledInterviewForm() reschedule_form = ScheduledInterviewForm()
reschedule_form.initial['topic'] = interview.interview.topic if interview:
meeting=interview.interview reschedule_form.initial['topic'] = interview.topic
reschedule_form.initial['start_time'] = interview.start_time
reschedule_form.initial['duration'] = interview.duration
context = { context = {
'schedule': schedule,
'interview': interview, 'interview': interview,
'reschedule_form':reschedule_form, 'reschedule_form':reschedule_form,
'interview_status_form':ScheduledInterviewUpdateStatusForm(), 'interview_status_form':ScheduledInterviewUpdateStatusForm(),
@ -6830,30 +6844,136 @@ def interview_add_note(request, slug):
form.initial['interview'] = interview form.initial['interview'] = interview
form.fields['interview'].widget = HiddenInput() form.fields['interview'].widget = HiddenInput()
form.fields['application'].widget = HiddenInput() form.fields['author'].widget = HiddenInput()
form.initial['author'] = request.user form.initial['author'] = request.user
form.fields['author'].widget = HiddenInput() form.fields['author'].widget = HiddenInput()
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()}) return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()})
# @login_required # Settings CRUD Views
# @staff_user_required @staff_user_required
# def archieve_application_bank_view(request): def settings_list(request):
# """View to list all applications in the application bank""" """List all settings with search and pagination"""
# applications = Application.objects.filter(stage="Applied").se search_query = request.GET.get('q', '')
# lect_related('person', 'job_posting').all().order_by('-created_at') settings = Settings.objects.all()
# paginator = Paginator(applications, 20) # Show 20 applications per page if search_query:
# page_number = request.GET.get('page') settings = settings.filter(key__icontains=search_query)
# page_obj = paginator.get_page(page_number)
# context = { # Order by key alphabetically
# 'page_obj': page_obj, settings = settings.order_by('key')
# 'applications': applications,
# } # Pagination
# return render(request, 'jobs/archieve_applications_bank.html', context) paginator = Paginator(settings, 20) # Show 20 settings per page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'search_query': search_query,
'total_settings': settings.count(),
}
return render(request, 'recruitment/settings_list.html', context)
@staff_user_required
def settings_create(request):
"""Create a new setting"""
if request.method == 'POST':
form = SettingsForm(request.POST)
if form.is_valid():
setting = form.save()
messages.success(request, f'Setting "{setting.key}" created successfully!')
return redirect('settings_list')
else:
messages.error(request, 'Please correct the errors below.')
else:
form = SettingsForm()
# In your views.py (or where the application views are defined) context = {
'form': form,
'title': 'Create New Setting',
'button_text': 'Create Setting',
}
return render(request, 'recruitment/settings_form.html', context)
@staff_user_required
def settings_detail(request, pk):
"""View details of a specific setting"""
setting = get_object_or_404(Settings, pk=pk)
context = {
'setting': setting,
}
return render(request, 'recruitment/settings_detail.html', context)
@staff_user_required
def settings_update(request, pk):
"""Update an existing setting"""
setting = get_object_or_404(Settings, pk=pk)
if request.method == 'POST':
form = SettingsForm(request.POST, instance=setting)
if form.is_valid():
form.save()
messages.success(request, f'Setting "{setting.key}" updated successfully!')
return redirect('settings_detail', pk=setting.pk)
else:
messages.error(request, 'Please correct the errors below.')
else:
form = SettingsForm(instance=setting)
context = {
'form': form,
'setting': setting,
'title': f'Edit Setting: {setting.key}',
'button_text': 'Update Setting',
}
return render(request, 'recruitment/settings_form.html', context)
@staff_user_required
def settings_delete(request, pk):
"""Delete a setting"""
setting = get_object_or_404(Settings, pk=pk)
if request.method == 'POST':
setting_name = setting.key
setting.delete()
messages.success(request, f'Setting "{setting_name}" deleted successfully!')
return redirect('settings_list')
context = {
'setting': setting,
'title': 'Delete Setting',
'message': f'Are you sure you want to delete the setting "{setting_name}"?',
'cancel_url': reverse('settings_detail', kwargs={'pk': setting.pk}),
}
return render(request, 'recruitment/settings_confirm_delete.html', context)
@staff_user_required
def settings_toggle_status(request, pk):
"""Toggle active status of a setting"""
setting = get_object_or_404(Settings, pk=pk)
if request.method == 'POST':
setting.is_active = not setting.is_active
setting.save(update_fields=['is_active'])
status_text = 'activated' if setting.is_active else 'deactivated'
messages.success(request, f'Setting "{setting.key}" {status_text} successfully!')
return redirect('settings_detail', pk=setting.pk)
# For GET requests or HTMX, return JSON response
if request.headers.get('HX-Request'):
return JsonResponse({
'success': True,
'is_active': setting.is_active,
'message': f'Setting "{setting.key}" {status_text} successfully!'
})
return redirect('settings_detail', pk=setting.pk)

View File

@ -1,19 +1,22 @@
import requests import requests
import jwt import jwt
import time import time
from .utils import get_zoom_config
ZOOM_API_KEY = 'your_zoom_api_key'
ZOOM_API_SECRET = 'your_zoom_api_secret'
def generate_zoom_jwt(): def generate_zoom_jwt():
"""Generate JWT token using dynamic Zoom configuration"""
config = get_zoom_config()
payload = { payload = {
'iss': ZOOM_API_KEY, 'iss': config['ZOOM_ACCOUNT_ID'],
'exp': time.time() + 3600 'exp': time.time() + 3600
} }
token = jwt.encode(payload, ZOOM_API_SECRET, algorithm='HS256') token = jwt.encode(payload, config['ZOOM_CLIENT_SECRET'], algorithm='HS256')
return token return token
def create_zoom_meeting(topic, start_time, duration, host_email): def create_zoom_meeting(topic, start_time, duration, host_email):
"""Create a Zoom meeting using dynamic configuration"""
jwt_token = generate_zoom_jwt() jwt_token = generate_zoom_jwt()
headers = { headers = {
'Authorization': f'Bearer {jwt_token}', 'Authorization': f'Bearer {jwt_token}',
@ -29,3 +32,36 @@ def create_zoom_meeting(topic, start_time, duration, host_email):
} }
url = f"https://api.zoom.us/v2/users/{host_email}/meetings" url = f"https://api.zoom.us/v2/users/{host_email}/meetings"
return requests.post(url, json=data, headers=headers) return requests.post(url, json=data, headers=headers)
def update_zoom_meeting(meeting_id, updated_data):
"""Update an existing Zoom meeting"""
jwt_token = generate_zoom_jwt()
headers = {
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
url = f"https://api.zoom.us/v2/meetings/{meeting_id}"
return requests.patch(url, json=updated_data, headers=headers)
def delete_zoom_meeting(meeting_id):
"""Delete a Zoom meeting"""
jwt_token = generate_zoom_jwt()
headers = {
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
url = f"https://api.zoom.us/v2/meetings/{meeting_id}"
return requests.delete(url, headers=headers)
def get_zoom_meeting_details(meeting_id):
"""Get details of a Zoom meeting"""
jwt_token = generate_zoom_jwt()
headers = {
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
url = f"https://api.zoom.us/v2/meetings/{meeting_id}"
return requests.get(url, headers=headers)

View File

@ -43,7 +43,6 @@
body { body {
min-height: 100vh; min-height: 100vh;
background-color: #f0f0f5;
padding-top: 0; padding-top: 0;
} }

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n crispy_forms_tags %} {% load static i18n crispy_forms_tags %}
{% block title %}{{ interview.application.name }} - {% trans "Interview Details" %} - ATS{% endblock %} {% block title %}{{ schedule.application.name }} - {% trans "Interview Details" %} - ATS{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
@ -213,14 +213,14 @@
{% trans "Interview Details" %} {% trans "Interview Details" %}
</h1> </h1>
<h2 class="h5 text-muted mb-0"> <h2 class="h5 text-muted mb-0">
{{ interview.application.name }} - {{ interview.job.title }} {{ schedule.application.name }} - {{ schedule.job.title }}
</h2> </h2>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'interview_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'interview_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Interviews" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Interviews" %}
</a> </a>
<a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'job_detail' schedule.job.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %} <i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
</a> </a>
{% if interview.status != 'cancelled' %} {% if interview.status != 'cancelled' %}
@ -241,18 +241,18 @@
<i class="fas fa-user me-2"></i> {% trans "Candidate Information" %} <i class="fas fa-user me-2"></i> {% trans "Candidate Information" %}
</h5> </h5>
<div class="action-buttons"> <div class="action-buttons">
{% if interview.application.resume %} {% if schedule.application.resume %}
<a href="{{ interview.application.resume.url }}" class="btn btn-outline-primary btn-sm" target="_blank"> <a href="{{ schedule.application.resume.url }}" class="btn btn-outline-primary btn-sm" target="_blank">
<i class="fas fa-download me-1"></i> {% trans "Download Resume" %} <i class="fas fa-download me-1"></i> {% trans "Download Resume" %}
</a> </a>
{% endif %} {% endif %}
<button type="button" class="btn btn-outline-primary btn-sm" {% comment %} <button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateModal" data-bs-target="#candidateModal"
hx-get="#" hx-get="#"
hx-target="#candidateModalBody"> hx-target="#candidateModalBody">
<i class="fas fa-eye me-1"></i> {% trans "AI Scoring" %} <i class="fas fa-eye me-1"></i> {% trans "AI Scoring" %}
</button> </button> {% endcomment %}
</div> </div>
</div> </div>
@ -260,23 +260,25 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="info-panel"> <div class="info-panel">
<h6>{% trans "Personal Details" %}</h6> <h6>{% trans "Personal Details" %}</h6>
<p class="mb-2"><strong>{% trans "Name:" %}</strong> {{ interview.application.name }}</p> <p class="mb-2"><strong>{% trans "Name:" %}</strong> {{ schedule.application.name }}</p>
<p class="mb-2"><strong>{% trans "Email:" %}</strong> {{ interview.application.email }}</p> <p class="mb-2"><strong>{% trans "Email:" %}</strong> {{ schedule.application.email }}</p>
<p class="mb-2"><strong>{% trans "Phone:" %}</strong> {{ interview.application.phone }}</p> <p class="mb-2"><strong>{% trans "Phone:" %}</strong> {{ schedule.application.phone }}</p>
{% if interview.application.location %} {% if schedule.application.location %}
<p class="mb-0"><strong>{% trans "Location:" %}</strong> {{ interview.application.location }}</p> <p class="mb-0"><strong>{% trans "Location:" %}</strong> {{ schedule.application.location }}</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="info-panel"> <div class="info-panel">
<h6>{% trans "Application Details" %}</h6> <h6>{% trans "Application Details" %}</h6>
<p class="mb-2"><strong>{% trans "Job:" %}</strong> {{ interview.job.title }}</p> <p class="mb-2"><strong>{% trans "Job:" %}</strong> {{ schedule.job.title }}</p>
<p class="mb-2"><strong>{% trans "Department:" %}</strong> {{ interview.job.department }}</p> <p class="mb-2"><strong>{% trans "Department:" %}</strong> {{ schedule.job.department }}</p>
<p class="mb-2"><strong>{% trans "Applied Date:" %}</strong> {{ interview.application.created_at|date:"d-m-Y" }}</p> <p class="mb-2"><strong>{% trans "Applied Date:" %}</strong> {{ schedule.application.created_at|date:"d-m-Y" }}</p>
<p class="mb-0"><strong>{% trans "Current Stage:" %}</strong> <p class="mb-0"><strong>{% trans "Current Stage:" %}</strong>
<span class="badge stage-badge bg-primary-theme"> <span class="badge stage-badge bg-primary-theme">
{{ interview.application.stage }} {{ interview.application.stage }}
<span class="badge stage-badge stage-{{ schedule.application.stage|lower }}">
{{ schedule.application.stage }}
</span> </span>
</p> </p>
</div> </div>
@ -291,22 +293,22 @@
</h5> </h5>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<span class="badge interview-type-badge <span class="badge interview-type-badge
{% if interview.interview.location_type == 'Remote' %}bg-remote {% if interview.location_type == 'Remote' %}bg-remote
{% else %}bg-onsite {% else %}bg-onsite
{% endif %}"> {% endif %}">
{% if interview.interview.location_type == 'Remote' %} {% if interview.location_type == 'Remote' %}
<i class="fas fa-video me-1"></i> {% trans "Remote" %} <i class="fas fa-video me-1"></i> {% trans "Remote" %}
{% else %} {% else %}
<i class="fas fa-building me-1"></i> {% trans "Onsite" %} <i class="fas fa-building me-1"></i> {% trans "Onsite" %}
{% endif %} {% endif %}
</span> </span>
<span class="badge status-badge <span class="badge status-badge
{% if interview.status == 'SCHEDULED' %}bg-scheduled {% if schedule.status == 'SCHEDULED' %}bg-scheduled
{% elif interview.status == 'CONFIRMED' %}bg-confirmed {% elif schedule.status == 'CONFIRMED' %}bg-confirmed
{% elif interview.status == 'CANCELLED' %}bg-cancelled {% elif schedule.status == 'CANCELLED' %}bg-cancelled
{% elif interview.status == 'COMPLETED' %}bg-completed {% elif schedule.status == 'COMPLETED' %}bg-completed
{% endif %}"> {% endif %}">
{{ interview.status }} {{ schedule.status }}
</span> </span>
</div> </div>
</div> </div>
@ -316,45 +318,45 @@
<div class="meeting-details"> <div class="meeting-details">
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Date:" %}</span> <span class="detail-label">{% trans "Date:" %}</span>
<span class="detail-value">{{ interview.interview_date|date:"d-m-Y" }}</span> <span class="detail-value">{{ schedule.interview_date|date:"d-m-Y" }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Time:" %}</span> <span class="detail-label">{% trans "Time:" %}</span>
<span class="detail-value">{{ interview.interview_time|date:"h:i A" }}</span> <span class="detail-value">{{ schedule.interview_time|date:"h:i A" }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Duration:" %}</span> <span class="detail-label">{% trans "Duration:" %}</span>
<span class="detail-value">{{ interview.interview.duration }} {% trans "minutes" %}</span> <span class="detail-value">{{ interview.duration }} {% trans "minutes" %}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Status:" %}</span> <span class="detail-label">{% trans "Status:" %}</span>
<span class="detail-value"> <span class="detail-value">
<span class="badge bg-primary-theme"> <span class="badge bg-primary-theme">
{{ interview.status }}</span> {{ schedule.status }}</span>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% if interview.interview.location_type == 'Remote' %} {% if interview.location_type == 'Remote' %}
<div class="meeting-details"> <div class="meeting-details">
<h6 class="mb-3">{% trans "Remote Meeting Details" %}</h6> <h6 class="mb-3">{% trans "Remote Meeting Details" %}</h6>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Platform:" %}</span> <span class="detail-label">{% trans "Platform:" %}</span>
<span class="detail-value">Zoom</span> <span class="detail-value">Zoom</span>
</div> </div>
{% if interview.interview %} {% if schedule.interview %}
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Meeting ID:" %}</span> <span class="detail-label">{% trans "Meeting ID:" %}</span>
<span class="detail-value">{{ interview.interview.meeting_id }}</span> <span class="detail-value">{{ interview.meeting_id }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Password:" %}</span> <span class="detail-label">{% trans "Password:" %}</span>
<span class="detail-value">{{ interview.interview.password }}</span> <span class="detail-value">{{ interview.password }}</span>
</div> </div>
{% if interview.interview.details_url %} {% if interview.details_url %}
<div class="mt-3"> <div class="mt-3">
<a href="{{ interview.interview.zoommeetingdetails.details_url }}" <a href="{{ interview.zoommeetingdetails.details_url }}"
target="_blank" target="_blank"
class="btn btn-main-action btn-sm w-100"> class="btn btn-main-action btn-sm w-100">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting" %} <i class="fas fa-video me-1"></i> {% trans "Join Meeting" %}
@ -366,14 +368,14 @@
{% else %} {% else %}
<div class="meeting-details"> <div class="meeting-details">
<h6 class="mb-3">{% trans "Onsite Location Details" %}</h6> <h6 class="mb-3">{% trans "Onsite Location Details" %}</h6>
{% if interview.interview %} {% if schedule.interview %}
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Address:" %}</span> <span class="detail-label">{% trans "Address:" %}</span>
<span class="detail-value">{{ interview.interview.physical_address }}</span> <span class="detail-value">{{ interview.physical_address }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">{% trans "Room:" %}</span> <span class="detail-label">{% trans "Room:" %}</span>
<span class="detail-value">{{ interview.interview.room_number }}</span> <span class="detail-value">{{ interview.room_number }}</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -392,13 +394,13 @@
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div> <div>
<h6 class="mb-1">{% trans "Interview Scheduled" %}</h6> <h6 class="mb-1">{% trans "Interview Scheduled" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"F j, Y" }} {{ interview.interview_time|date:"h:i A" }}</p> <p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ schedule.interview_date|date:"d-m-Y" }} {{ schedule.interview_time|date:"h:i A" }}</p>
</div> </div>
<small class="text-muted">{{ interview.interview.created_at|date:"F j,Y h:i A" }}</small> <small class="text-muted">{{ interview.created_at|date:"d-m-Y h:i A" }}</small>
</div> </div>
</div> </div>
</div> </div>
{% if interview.status == 'confirmed' %} {% if interview.status == 'CONFIRMED' %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content"> <div class="timeline-content">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@ -411,7 +413,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if interview.status == 'completed' %} {% if interview.status == 'COMPLETED' %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content"> <div class="timeline-content">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@ -424,7 +426,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if interview.status == 'cancelled' %} {% if interview.status == 'CANCELLED' %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content"> <div class="timeline-content">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@ -432,7 +434,7 @@
<h6 class="mb-1">{% trans "Interview Cancelled" %}</h6> <h6 class="mb-1">{% trans "Interview Cancelled" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview was cancelled on: " %}{{interview.interview.cancelled_at|date:"F j, Y"}}</p> <p class="mb-0 text-muted">{% trans "Interview was cancelled on: " %}{{interview.interview.cancelled_at|date:"F j, Y"}}</p>
</div> </div>
<small class="text-muted">{% trans "Recently" %}</small>
</div> </div>
</div> </div>
</div> </div>
@ -442,14 +444,16 @@
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<!-- Participants Panel -->
{% comment %} <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;"> <h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-users me-2"></i> {% trans "Participants" %} <i class="fas fa-users me-2"></i> {% trans "Participants" %}
</h5> </h5>
{% if interview.participants.exists %} <!-- Internal Participants -->
{% if schedule.participants.exists %}
<h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6> <h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6>
{% for participant in interview.participants.all %} {% for participant in schedule.participants.all %}
<div class="participant-item"> <div class="participant-item">
<div class="participant-avatar"> <div class="participant-avatar">
{{ participant.first_name.0 }}{{ participant.last_name.0 }} {{ participant.first_name.0 }}{{ participant.last_name.0 }}
@ -462,9 +466,10 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if interview.system_users.exists %} <!-- External Participants -->
{% if schedule.system_users.exists %}
<h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6> <h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6>
{% for user in interview.system_users.all %} {% for user in schedule.system_users.all %}
<div class="participant-item"> <div class="participant-item">
<div class="participant-avatar"> <div class="participant-avatar">
{{ user.first_name.0 }}{{ user.last_name.0 }} {{ user.first_name.0 }}{{ user.last_name.0 }}
@ -477,7 +482,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if not interview.participants.exists and not interview.system_users.exists %} {% if not schedule.participants.exists and not schedule.system_users.exists %}
<div class="text-center py-3 text-muted"> <div class="text-center py-3 text-muted">
<i class="fas fa-users fa-2x mb-2"></i> <i class="fas fa-users fa-2x mb-2"></i>
<p class="mb-0">{% trans "No participants added yet" %}</p> <p class="mb-0">{% trans "No participants added yet" %}</p>
@ -490,6 +495,7 @@
<i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %} <i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %}
</button> </button>
</div> {% endcomment %} </div> {% endcomment %}
</div> {% endcomment %}
<div class="kaauh-card shadow-sm p-4"> <div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;"> <h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
@ -497,9 +503,7 @@
</h5> </h5>
<div class="action-buttons"> <div class="action-buttons">
{% if schedule.status != 'CANCELLED' and schedule.status != 'COMPLETED' %}
{% if interview.status != 'cancelled' and interview.status != 'COMPLETED' %}
{# 1. Interview is Active (SCHEDULED/CONFIRMED) #}
<button type="button" class="btn btn-main-action btn-sm" <button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#rescheduleModal"> data-bs-target="#rescheduleModal">
@ -509,35 +513,7 @@
<button type="button" class="btn btn-outline-danger btn-sm" <button type="button" class="btn btn-outline-danger btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#cancelModal"> data-bs-target="#cancelModal">
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %} <i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</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> </button>
{% endif %} {% endif %}
@ -548,6 +524,14 @@
data-bs-target="#emailModal"> data-bs-target="#emailModal">
<i class="fas fa-envelope me-1"></i> {% trans "Send Email" %} <i class="fas fa-envelope me-1"></i> {% trans "Send Email" %}
</button> </button>
{% if schedule.status == 'COMPLETED' %}
<button type="button" class="btn btn-outline-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#resultModal">
<i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %}
</button>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -618,29 +602,29 @@
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label for="email_to" class="form-label">{% trans "To" %}</label> <label for="email_to" class="form-label">{% trans "To" %}</label>
<input type="email" class="form-control" id="email_to" value="{{ interview.application.email }}" readonly> <input type="email" class="form-control" id="email_to" value="{{ schedule.application.email }}" readonly>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email_subject" class="form-label">{% trans "Subject" %}</label> <label for="email_subject" class="form-label">{% trans "Subject" %}</label>
<input type="text" class="form-control" id="email_subject" name="subject" <input type="text" class="form-control" id="email_subject" name="subject"
value="{% trans 'Interview Details' %} - {{ interview.job.title }}"> value="{% trans 'Interview Details' %} - {{ schedule.job.title }}">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email_message" class="form-label">{% trans "Message" %}</label> <label for="email_message" class="form-label">{% trans "Message" %}</label>
<textarea class="form-control" id="email_message" name="message" rows="6"> <textarea class="form-control" id="email_message" name="message" rows="6">
{% trans "Dear" %} {{ interview.application.name }}, {% trans "Dear" %} {{ schedule.application.name }},
{% trans "Your interview details are as follows:" %} {% trans "Your interview details are as follows:" %}
{% trans "Date:" %} {{ interview.interview_date|date:"d-m-Y" }} {% trans "Date:" %} {{ schedule.interview_date|date:"d-m-Y" }}
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }} {% trans "Time:" %} {{ schedule.interview_time|date:"h:i A" }}
{% trans "Job:" %} {{ interview.job.title }} {% trans "Job:" %} {{ schedule.job.title }}
{% if interview.interview.location_type == 'Remote' %} {% if interview.location_type == 'Remote' %}
{% trans "This is a remote interview. You will receive the meeting link separately." %} {% trans "This is a remote schedule. You will receive the meeting link separately." %}
{% else %} {% else %}
{% trans "This is an onsite interview. Please arrive 10 minutes early." %} {% trans "This is an onsite schedule. Please arrive 10 minutes early." %}
{% endif %} {% endif %}
{% trans "Best regards," %} {% trans "Best regards," %}
{% trans "HR Team" %} {% trans "HR Team" %}
@ -665,11 +649,10 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="post" action="{% url 'reschedule_meeting_for_application' interview.slug %}"> <form method="post" action="{% url 'reschedule_meeting_for_application' schedule.slug %}">
{% csrf_token %} {% csrf_token %}
{{ reschedule_form|crispy }} {{reschedule_form|crispy}}
<button type="submit" class="btn btn-main-action btn-sm">
<button type="submit" class="btn btn-main-action btn-sm mt-3">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %} <i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button> </button>
</form> </form>
@ -688,7 +671,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="post" action="{% url 'cancel_interview_for_application' interview.interview.slug %}"> <form method="post" action="{% url 'cancel_interview_for_application' schedule.slug %}">
{% csrf_token %} {% csrf_token %}
{{ cancel_form|crispy }} {{ cancel_form|crispy }}
<div class="alert alert-warning"> <div class="alert alert-warning">
@ -754,7 +737,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="post" action="{% url 'update_interview_status' interview.slug %}"> <form method="post" action="{% url 'update_interview_status' schedule.slug %}">
{% csrf_token %} {% csrf_token %}
{{ interview_status_form|crispy }} {{ interview_status_form|crispy }}
<div class="d-flex gap-2 mt-3 justify-content-end"> <div class="d-flex gap-2 mt-3 justify-content-end">
@ -793,9 +776,6 @@ document.addEventListener('DOMContentLoaded', function () {
</div> </div>
`; `;
} }
// Note: For forms using Django/Crispy, you might need extra JS
// to explicitly reset form fields if the form is not reloading
// when the modal is closed.
}); });
} }
}); });

View File

@ -18,7 +18,6 @@
} }
body { body {
background-color: #f8f9fa; /* Subtle light background */
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }

View File

@ -25,7 +25,7 @@
<label for="{{ form.status.id_for_label }}" class="form-label small"> <label for="{{ form.status.id_for_label }}" class="form-label small">
<i class="fas fa-info-circle me-1"></i> {% trans "Meeting Status" %} <i class="fas fa-info-circle me-1"></i> {% trans "Meeting Status" %}
</label> </label>
{{ form.status|add_class:"form-select" }} {{ form.status }}
{% for error in form.status.errors %} {% for error in form.status.errors %}
<div class="text-danger small mt-1">{{ error }}</div> <div class="text-danger small mt-1">{{ error }}</div>
{% endfor %} {% endfor %}

View File

@ -1,5 +1,5 @@
{% extends "portal_base.html" %} {% extends "portal_base.html" %}
{% load static %} {% load static i18n %}
{% block title %}{{ message.subject }}{% endblock %} {% block title %}{{ message.subject }}{% endblock %}
@ -9,40 +9,49 @@
<div class="col-12"> <div class="col-12">
<!-- Message Header --> <!-- Message Header -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header bg-gradient border-0 pb-3">
<h5 class="mb-0"> <div class="d-flex justify-content-between align-items-start gap-3">
{{ message.subject }} <div>
<h4 class="mb-2 fw-bold">{{ message.subject }}</h4>
{% if message.parent_message %} {% if message.parent_message %}
<span class="badge bg-secondary ms-2">Reply</span> <span class="badge bg-info">{% trans "Reply" %}</span>
{% endif %} {% endif %}
</h5> </div>
<div class="btn-group" role="group"> <div class="btn-group flex-wrap" role="group">
<a href="{% url 'message_reply' message.id %}" class="btn btn-outline-info"> <a href="{% url 'message_reply' message.id %}" class="btn btn-sm btn-main-action">
<i class="fas fa-reply"></i> Reply <i class="fas fa-reply me-1"></i> {% trans "Reply" %}
</a> </a>
{% if message.recipient == request.user %} {% comment %} {% if message.recipient == request.user %}
<a href="{% url 'message_mark_unread' message.id %}" <button type="button" class="btn btn-sm btn-outline-warning"
class="btn btn-outline-warning" hx-post="{% url 'message_mark_unread' message.id %}"
hx-post="{% url 'message_mark_unread' message.id %}"> hx-swap="outerHTML">
<i class="fas fa-envelope"></i> Mark Unread <i class="fas fa-envelope me-1"></i> {% trans "Mark Unread" %}
</a> </button>
{% endif %} {% endif %} {% endcomment %}
<a href="{% url 'message_delete' message.id %}" {% comment %} <button type="button" class="btn btn-sm btn-outline-danger"
class="btn btn-outline-danger" hx-post="{% url 'message_delete' message.id %}"
hx-get="{% url 'message_delete' message.id %}" hx-confirm="{% trans 'Are you sure you want to permanently delete this message? This action cannot be undone.' %}"
hx-confirm="Are you sure you want to delete this message?"> hx-redirect="{% url 'message_list' %}"
<i class="fas fa-trash"></i> Delete onclick="this.disabled=true;">
</a> <i class="fas fa-trash me-1"></i> {% trans "Delete" %}
<a href="{% url 'message_list' %}" class="btn btn-outline-secondary"> </button> {% endcomment %}
<i class="fas fa-arrow-left"></i> Back to Messages <a href="{% url 'message_list' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back" %}
</a> </a>
</div> </div>
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<strong>From:</strong> <strong>From:</strong>
<span class="text-primary">{{ message.sender.get_full_name|default:message.sender.username }}</span> <span class="text-primary">
{% if message.sender == request.user %}
{% trans "Me"%}
{% else %}
{{ message.sender.get_full_name|default:message.sender.username }}
{% endif %}
</span>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<strong>To:</strong> <strong>To:</strong>
@ -57,9 +66,9 @@
</span> </span>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<strong>Status:</strong> <strong>{% trans "Status"%}:</strong>
{% if message.is_read %} {% if message.is_read %}
<span class="badge bg-success">Read</span> <span class="badge bg-success">{% trans "Read"%}</span>
{% if message.read_at %} {% if message.read_at %}
<small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small> <small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small>
{% endif %} {% endif %}
@ -76,9 +85,7 @@
{% if message.job %} {% if message.job %}
<div class="col-md-6"> <div class="col-md-6">
<strong>Related Job:</strong> <strong>Related Job:</strong>
<a href="{% url 'job_detail' message.job.slug %}" class="text-primary">
{{ message.job.title }} {{ message.job.title }}
</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,5 +1,5 @@
{% extends "portal_base.html" %} {% extends "portal_base.html" %}
{% load static %} {% load static i18n %}
{% block title %}Messages{% endblock %} {% block title %}Messages{% endblock %}
@ -103,8 +103,21 @@
<span class="badge bg-secondary ms-2">Reply</span> <span class="badge bg-secondary ms-2">Reply</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ message.sender.get_full_name|default:message.sender.username }}</td> <td>
<td>{{ message.recipient.get_full_name|default:message.recipient.username }}</td> {% if message.sender == request.user %}
{% trans "Me"%}
{% else %}
{{ message.sender.get_full_name|default:message.sender.username }}
{% endif %}
</td>
<td>
{% if message.recipient == request.user %}
{% trans "Me"%}
{% else %}
{{ message.recipient.get_full_name|default:message.recipient.username }}
{% endif %}
</td>
<td>
<td> <td>
<span> <span>
{{ message.get_message_type_display }} {{ message.get_message_type_display }}
@ -127,7 +140,6 @@
{% if not message.is_read and message.recipient == request.user %} {% if not message.is_read and message.recipient == request.user %}
<a href="{% url 'message_mark_read' message.id %}" <a href="{% url 'message_mark_read' message.id %}"
class="btn btn-sm btn-outline-success" class="btn btn-sm btn-outline-success"
hx-post="{% url 'message_mark_read' message.id %}"
title="Mark as Read"> title="Mark as Read">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</a> </a>
@ -136,13 +148,13 @@
class="btn btn-sm btn-outline-primary" title="Reply"> class="btn btn-sm btn-outline-primary" title="Reply">
<i class="fas fa-reply"></i> <i class="fas fa-reply"></i>
</a> </a>
<a href="{% url 'message_delete' message.id %}" {% comment %} <a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}" hx-get="{% url 'message_delete' message.id %}"
hx-confirm="Are you sure you want to delete this message?" hx-confirm="Are you sure you want to delete this message?"
title="Delete"> title="Delete">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a> {% endcomment %}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -28,13 +28,13 @@
<i class="fas fa-envelope me-1"></i> {% trans "Mark Unread" %} <i class="fas fa-envelope me-1"></i> {% trans "Mark Unread" %}
</button> </button>
{% endif %} {% endif %}
<button type="button" class="btn btn-sm btn-outline-danger" {% comment %} <button type="button" class="btn btn-sm btn-outline-danger"
hx-post="{% url 'message_delete' message.id %}" hx-post="{% url 'message_delete' message.id %}"
hx-confirm="{% trans 'Are you sure you want to permanently delete this message? This action cannot be undone.' %}" hx-confirm="{% trans 'Are you sure you want to permanently delete this message? This action cannot be undone.' %}"
hx-redirect="{% url 'message_list' %}" hx-redirect="{% url 'message_list' %}"
onclick="this.disabled=true;"> onclick="this.disabled=true;">
<i class="fas fa-trash me-1"></i> {% trans "Delete" %} <i class="fas fa-trash me-1"></i> {% trans "Delete" %}
</button> </button> {% endcomment %}
<a href="{% url 'message_list' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'message_list' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back" %}
</a> </a>

View File

@ -17,7 +17,7 @@
<div class="card mb-4 border-primary-theme-subtle"> <div class="card mb-4 border-primary-theme-subtle">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-3"> <div class="col-md-2">
<label for="status" class="form-label">{% trans "Status" %}</label> <label for="status" class="form-label">{% trans "Status" %}</label>
<select name="status" id="status" class="form-select"> <select name="status" id="status" class="form-select">
<option value="">{% trans "All Status" %}</option> <option value="">{% trans "All Status" %}</option>
@ -25,16 +25,26 @@
<option value="unread" {% if status_filter == 'unread' %}selected{% endif %}>{% trans "Unread" %}</option> <option value="unread" {% if status_filter == 'unread' %}selected{% endif %}>{% trans "Unread" %}</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<label for="type" class="form-label">{% trans "Type" %}</label> <label for="type" class="form-label">{% trans "Type" %}</label>
<select name="type" id="type" class="form-select"> <select name="type" id="type" class="form-select">
<option value="">{% trans "All Types" %}</option> <option value="">{% trans "All Types" %}</option>
<option value="GENERAL" {% if type_filter == 'GENERAL' %}selected{% endif %}>{% trans "General" %}</option> <option value="direct" {% if type_filter == 'DIRECT' %}selected{% endif %}>{% trans "Direct" %}</option>
<option value="JOB_RELATED" {% if type_filter == 'JOB_RELATED' %}selected{% endif %}>{% trans "Job Related" %}</option> <option value="job_related" {% if type_filter == 'JOB_RELATED' %}selected{% endif %}>{% trans "Job Related" %}</option>
<option value="INTERVIEW" {% if type_filter == 'INTERVIEW' %}selected{% endif %}>{% trans "Interview" %}</option> <option value="system" {% if type_filter == 'SYSTEM' %}selected{% endif %}>{% trans "System" %}</option>
<option value="OFFER" {% if type_filter == 'OFFER' %}selected{% endif %}>{% trans "Offer" %}</option>
</select> </select>
</div> </div>
{% if user.user_type == "staff" %}
<div class="col-md-3">
<label for="job" class="form-label">{% trans "Job" %}</label>
<select name="job_filter" id="job" class="form-select">
<option value="">{% trans "All Types" %}</option>
{% for job in jobs %}
<option value="{{ job.pk }}" {% if job_filter == job.pk %}selected{% endif %}>{{job.title}}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="col-md-4"> <div class="col-md-4">
<label for="q" class="form-label">{% trans "Search" %}</label> <label for="q" class="form-label">{% trans "Search" %}</label>
<div class="input-group"> <div class="input-group">
@ -102,8 +112,20 @@
<span class="badge bg-secondary-theme ms-2 text-decoration-none">{% trans "Reply" %}</span> <span class="badge bg-secondary-theme ms-2 text-decoration-none">{% trans "Reply" %}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ message.sender.get_full_name|default:message.sender.username }}</td> <td>
<td>{{ message.recipient.get_full_name|default:message.recipient.username }}</td> {% if message.sender == request.user %}
{% trans "Me"%}
{% else %}
{{ message.sender.get_full_name|default:message.sender.username }}
{% endif %}
</td>
<td>
{% if message.recipient == request.user %}
{% trans "Me"%}
{% else %}
{{ message.recipient.get_full_name|default:message.recipient.username }}
{% endif %}
</td>
<td> <td>
<span> <span>
{{ message.get_message_type_display }} {{ message.get_message_type_display }}
@ -126,7 +148,6 @@
{% if not message.is_read and message.recipient == request.user %} {% if not message.is_read and message.recipient == request.user %}
<a href="{% url 'message_mark_read' message.id %}" <a href="{% url 'message_mark_read' message.id %}"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
hx-post="{% url 'message_mark_read' message.id %}"
title="{% trans 'Mark as Read' %}"> title="{% trans 'Mark as Read' %}">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</a> </a>
@ -135,13 +156,13 @@
class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}"> class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}">
<i class="fas fa-reply"></i> <i class="fas fa-reply"></i>
</a> </a>
<a href="{% url 'message_delete' message.id %}" {% comment %} <a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
hx-post="{% url 'message_delete' message.id %}" hx-post="{% url 'message_delete' message.id %}"
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}" hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
title="{% trans 'Delete' %}"> title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a> {% endcomment %}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -38,7 +38,7 @@
/* Consistent, reduced padding for compact look */ /* Consistent, reduced padding for compact look */
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'settings_list' %}" class="text-decoration-none text-secondary">
<i class="fas fa-cog me-1"></i> Settings
</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'settings_detail' setting.pk %}" class="text-decoration-none text-secondary">
{{ setting.key }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page"
style="color: #F43B5E; font-weight: 600;">Delete</li>
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-primary-theme">
<i class="fas fa-exclamation-triangle me-2"></i>
{{ title }}
</h1>
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Cancel
</a>
</div>
<!-- Warning Card -->
<div class="card shadow-sm border-danger">
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<!-- Warning Alert -->
<div class="alert alert-warning d-flex align-items-center" role="alert">
<i class="fas fa-exclamation-triangle me-3 fa-lg"></i>
<div>
<strong>Warning:</strong> This action cannot be undone. Deleting this setting will permanently remove it from the system.
</div>
</div>
<!-- Setting Details -->
<div class="row">
<div class="col-12">
<h5 class="mb-3">Setting Details:</h5>
<div class="table-responsive">
<table class="table table-hover">
<tbody>
<tr>
<th style="width: 150px;" class="text-primary-theme">Key:</th>
<td><code class="text-primary-theme">{{ setting.key }}</code></td>
</tr>
<tr>
<th class="text-primary-theme">Value:</th>
<td>{{ setting.value|default:"-" }}</td>
</tr>
<tr>
<th class="text-primary-theme">Description:</th>
<td>{{ setting.description|default:"No description" }}</td>
</tr>
<tr>
<th class="text-primary-theme">Type:</th>
<td>
<span class="badge bg-primary-theme text-white">
{{ setting.get_type_display }}
</span>
</td>
</tr>
<tr>
<th class="text-primary-theme">Status:</th>
<td>
{% if setting.is_active %}
<span class="badge bg-primary-theme text-white">Active</span>
{% else %}
<span class="badge bg-secondary text-white">Inactive</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mt-4">
<div class="col-12">
<form method="post" onsubmit="return confirm('Are you absolutely sure you want to delete this setting? This action cannot be undone.');">
{% csrf_token %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> Yes, Delete Setting
</button>
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}Setting Details{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'settings_list' %}" class="text-decoration-none text-secondary">
<i class="fas fa-cog me-1"></i> Settings
</a>
</li>
<li class="breadcrumb-item active" aria-current="page"
style="color: #F43B5E; font-weight: 600;">{{ setting.key }}</li>
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-primary-theme">
<i class="fas fa-cog me-2"></i>
Setting Details
</h1>
<div class="d-flex gap-2">
<a href="{% url 'settings_update' pk=setting.pk %}" class="btn btn-main-action">
<i class="fas fa-edit me-1"></i> Edit Setting
</a>
<a href="{% url 'settings_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back to List
</a>
</div>
</div>
<!-- Setting Details Card -->
<div class="card shadow-sm">
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<div class="row">
<div class="col-md-6">
<h6 class="text-primary-theme"><strong>Key:</strong></h6>
<p><code class="text-primary-theme">{{ setting.key }}</code></p>
</div>
<div class="col-md-6">
<h6 class="text-primary-theme"><strong>Value:</strong></h6>
<p>{{ setting.value|default:"-" }}</p>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<h6 class="text-primary-theme"><strong>Created:</strong></h6>
<p>{{ setting.created_at|date:"Y-m-d H:i" }}</p>
</div>
<div class="col-md-4">
<h6 class="text-primary-theme"><strong>Last Updated:</strong></h6>
<p>{{ setting.updated_at|date:"Y-m-d H:i" }}</p>
</div>
<div class="col-md-4">
<h6 class="text-primary-theme"><strong>Updated By:</strong></h6>
<p>{{ setting.updated_by.get_full_name|default:"System" }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,218 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}{{ title }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAUH Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-gray-light: #f8f9fa;
}
/* Form Container Styling */
.form-container {
max-width: 800px;
margin: 0 auto;
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1.5rem;
}
.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);
}
/* Secondary Button Style */
.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);
}
/* Form Field Styling */
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Alert Styling */
.alert {
border-radius: 0.5rem;
border: none;
}
/* Loading State */
.btn.loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="form-container">
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'settings_list' %}" class="text-decoration-none text-secondary">
<i class="fas fa-cog me-1"></i> Settings
</a>
</li>
{% if setting %}
<li class="breadcrumb-item">
<a href="{% url 'settings_detail' setting.pk %}" class="text-decoration-none text-secondary">
{{ setting.key }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page"
style="color: #F43B5E; font-weight: 600;">Update</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page"
style="color: #F43B5E; font-weight: 600;">Create</li>
{% endif %}
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-cog me-2"></i> {{ title }}
</h4>
<div class="d-flex gap-2">
{% if setting %}
<a href="{% url 'settings_detail' setting.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-eye me-1"></i> View Details
</a>
<a href="{% url 'settings_delete' setting.pk %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> Delete
</a>
{% endif %}
<a href="{% url 'settings_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back to List
</a>
</div>
</div>
<!-- Form Card -->
<div class="card shadow-sm">
<div class="card-body p-4">
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>Error
</h5>
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" novalidate id="settings-form">
{% csrf_token %}
{{form|crispy}}
<div class="d-flex gap-2">
<button form="settings-form" type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {{ button_text }}
</button>
<a href="{% url 'settings_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-adjust textarea height
const textarea = document.querySelector('textarea[name="description"]');
if (textarea) {
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,186 @@
{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}Settings{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-primary-theme">
<i class="fas fa-cog me-2"></i>
Settings Management
</h1>
<a href="{% url 'settings_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-2"></i>
Create New Setting
</a>
</div>
<!-- Search and Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" name="q"
placeholder="Search settings..." value="{{ search_query }}">
</div>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search"></i> Search
</button>
{% if search_query %}
<a href="{% url 'settings_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times"></i> Clear
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- Results Summary -->
{% if search_query %}
<div class="alert alert-info">
Found {{ page_obj.paginator.count }} setting{{ page_obj.paginator.count|pluralize }} matching "{{ search_query }}"
</div>
{% endif %}
<!-- Settings Table -->
<div class="card">
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th style="width: 45%;">Key</th>
<th style="width: 45%;">Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for setting in page_obj %}
<tr>
<td>
<code class="text-primary-theme">{{ setting.key }}</code>
</td>
<td>{{ setting.value|truncatechars:50 }}</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'settings_detail' pk=setting.pk %}"
class="btn btn-sm btn-outline-primary" title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'settings_update' pk=setting.pk %}"
class="btn btn-sm btn-outline-secondary" title="Edit Setting">
<i class="fas fa-edit"></i>
</a>
<form method="post" action="{% url 'settings_delete' pk=setting.pk %}"
onsubmit="return confirm('Are you sure you want to delete this setting?');"
style="display: inline;">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete Setting">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Settings pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}">
<i class="fas fa-angle-double-left"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">
<i class="fas fa-angle-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if search_query %}&q={{ search_query }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">
<i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}">
<i class="fas fa-angle-double-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-cog fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No settings found</h5>
<p class="text-muted">
{% if search_query %}
No settings match your search criteria "{{ search_query }}".
{% else %}
Get started by creating your first setting.
{% endif %}
</p>
<a href="{% url 'settings_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> Create Setting
</a>
</div>
{% endif %}
<div class="text-center mt-3">
<small class="text-muted">
{% if page_obj %}
Showing {{ page_obj.start_index }}-{{ page_obj.end_index }} of {{ page_obj.paginator.count }} settings
{% if search_query %}
(filtered by: "{{ search_query }}")
{% endif %}
{% endif %}
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

169
test_job_reminders.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python
"""
Test script for job closing reminder system
"""
import os
import sys
import django
from datetime import datetime, timedelta
from django.utils import timezone
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
django.setup()
from recruitment.models import JobPosting, Application
from django_q.models import Schedule
from django.contrib.auth import get_user_model
User = get_user_model()
def test_reminder_scheduling():
"""Test that reminders are properly scheduled when a job is created/updated"""
print("🧪 Testing Job Closing Reminder System")
print("=" * 50)
# Get or create a test user
test_user, created = User.objects.get_or_create(
username='test_recruiter',
defaults={
'email': 'test@example.com',
'is_staff': True
}
)
# Create a test job with deadline in 2 days
deadline = timezone.now() + timedelta(days=2)
print(f"📅 Creating test job with deadline: {deadline}")
job = JobPosting.objects.create(
title='Test Software Engineer Position',
description='Test job for reminder system',
qualifications='Python, Django, PostgreSQL',
status='ACTIVE',
application_deadline=deadline,
assigned_to=test_user,
created_by='Test User'
)
print(f"✅ Created job: {job.title} (ID: {job.pk})")
# Check if schedules were created
schedules = Schedule.objects.filter(
name__in=[
f"one_day_reminder_{job.pk}",
f"fifteen_min_reminder_{job.pk}",
f"job_closing_{job.pk}"
]
)
print(f"\n📋 Scheduled Tasks ({schedules.count()} found):")
for schedule in schedules:
print(f" - {schedule.name}: {schedule.next_run}")
# Test deadline update
print(f"\n🔄 Testing deadline update...")
new_deadline = deadline + timedelta(hours=6)
job.application_deadline = new_deadline
job.save()
# Check if schedules were updated
updated_schedules = Schedule.objects.filter(
name__in=[
f"one_day_reminder_{job.pk}",
f"fifteen_min_reminder_{job.pk}",
f"job_closing_{job.pk}"
]
)
print(f"📋 Updated Scheduled Tasks ({updated_schedules.count()} found):")
for schedule in updated_schedules:
print(f" - {schedule.name}: {schedule.next_run}")
# Test job deactivation
print(f"\n🛑 Testing job deactivation...")
job.status = 'CLOSED'
job.save()
remaining_schedules = Schedule.objects.filter(
name__in=[
f"one_day_reminder_{job.pk}",
f"fifteen_min_reminder_{job.pk}",
f"job_closing_{job.pk}"
]
)
print(f"📋 Remaining Scheduled Tasks after deactivation: {remaining_schedules.count()}")
# Cleanup
print(f"\n🧹 Cleaning up test data...")
job.delete()
print("✅ Test completed successfully!")
def test_email_functions():
"""Test the email functions directly"""
print("\n🧪 Testing Email Functions")
print("=" * 50)
# Create a test job
test_user, _ = User.objects.get_or_create(
username='test_recruiter_email',
defaults={'email': 'test_email@example.com', 'is_staff': True}
)
job = JobPosting.objects.create(
title='Test Email Job',
description='Test job for email functions',
status='ACTIVE',
application_deadline=timezone.now() + timedelta(hours=1),
assigned_to=test_user,
created_by='Test User'
)
print(f"📧 Testing email functions for job: {job.pk}")
# Import and test email functions
from recruitment.tasks import (
send_one_day_reminder,
send_fifteen_minute_reminder,
send_job_closed_notification
)
try:
print("📤 Testing 1-day reminder...")
send_one_day_reminder(job.pk)
print("✅ 1-day reminder sent successfully")
except Exception as e:
print(f"❌ 1-day reminder failed: {e}")
try:
print("📤 Testing 15-minute reminder...")
send_fifteen_minute_reminder(job.pk)
print("✅ 15-minute reminder sent successfully")
except Exception as e:
print(f"❌ 15-minute reminder failed: {e}")
try:
print("📤 Testing job closed notification...")
send_job_closed_notification(job.pk)
print("✅ Job closed notification sent successfully")
except Exception as e:
print(f"❌ Job closed notification failed: {e}")
# Cleanup
job.delete()
print("🧹 Test data cleaned up")
if __name__ == '__main__':
try:
test_reminder_scheduling()
test_email_functions()
print("\n🎉 All tests completed!")
except Exception as e:
print(f"\n❌ Test failed with error: {e}")
import traceback
traceback.print_exc()