542 lines
18 KiB
Python
542 lines
18 KiB
Python
import logging
|
|
|
|
from datetime import datetime, timedelta
|
|
from django.db import transaction
|
|
from django_q.models import Schedule
|
|
from django_q.tasks import schedule
|
|
from django.dispatch import receiver
|
|
from django_q.tasks import async_task
|
|
from django.db.models.signals import post_save
|
|
from django.contrib.auth.models import User
|
|
from django.utils import timezone
|
|
from .models import (
|
|
FormField,
|
|
FormStage,
|
|
FormTemplate,
|
|
Application,
|
|
JobPosting,
|
|
Notification,
|
|
HiringAgency,
|
|
Person,
|
|
Source,
|
|
)
|
|
from .forms import generate_api_key, generate_api_secret
|
|
from django.contrib.auth import get_user_model
|
|
from django_q.models import Schedule
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
@receiver(post_save, sender=JobPosting)
|
|
def format_job(sender, instance, created, **kwargs):
|
|
if created or not instance.ai_parsed:
|
|
form = getattr(instance, "form_template", None)
|
|
if not form:
|
|
FormTemplate.objects.get_or_create(
|
|
job=instance, is_active=True, name=instance.title
|
|
)
|
|
async_task(
|
|
"recruitment.tasks.format_job_description",
|
|
instance.pk,
|
|
# hook='myapp.tasks.email_sent_callback' # Optional callback
|
|
)
|
|
|
|
# Enhanced reminder scheduling logic
|
|
if instance.status == "ACTIVE" and instance.application_deadline:
|
|
# Schedule 1-day reminder
|
|
one_day_schedule = Schedule.objects.filter(
|
|
name=f"one_day_reminder_{instance.pk}"
|
|
).first()
|
|
|
|
one_day_before = instance.application_deadline - timedelta(days=1)
|
|
if not one_day_schedule:
|
|
schedule(
|
|
"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,
|
|
schedule_type=Schedule.ONCE,
|
|
next_run=instance.application_deadline,
|
|
repeats=-1,
|
|
name=f"job_closing_{instance.pk}",
|
|
)
|
|
elif closing_schedule.next_run != instance.application_deadline:
|
|
closing_schedule.next_run = instance.application_deadline
|
|
closing_schedule.save()
|
|
|
|
else:
|
|
# Clean up all reminder schedules if job is no longer active
|
|
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)
|
|
# def update_form_template_status(sender, instance, created, **kwargs):
|
|
# if not created:
|
|
# if instance.status == "Active":
|
|
# instance.form_template.is_active = True
|
|
# else:
|
|
# instance.form_template.is_active = False
|
|
# instance.save()
|
|
|
|
|
|
@receiver(post_save, sender=Application)
|
|
def score_candidate_resume(sender, instance, created, **kwargs):
|
|
if instance.resume and not instance.is_resume_parsed:
|
|
logger.info(f"Scoring resume for candidate {instance.pk}")
|
|
async_task(
|
|
"recruitment.tasks.handle_resume_parsing_and_scoring",
|
|
instance.pk,
|
|
hook="recruitment.hooks.callback_ai_parsing",
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=FormTemplate)
|
|
def create_default_stages(sender, instance, created, **kwargs):
|
|
"""
|
|
Create default resume stages when a new FormTemplate is created
|
|
"""
|
|
if created:
|
|
with transaction.atomic():
|
|
# Stage 1: Contact Information
|
|
contact_stage = FormStage.objects.create(
|
|
template=instance,
|
|
name="Contact Information",
|
|
order=0,
|
|
is_predefined=True,
|
|
)
|
|
# FormField.objects.create(
|
|
# stage=contact_stage,
|
|
# label="First Name",
|
|
# field_type="text",
|
|
# required=True,
|
|
# order=0,
|
|
# is_predefined=True,
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=contact_stage,
|
|
# label="Last Name",
|
|
# field_type="text",
|
|
# required=True,
|
|
# order=1,
|
|
# is_predefined=True,
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=contact_stage,
|
|
# label="Email Address",
|
|
# field_type="email",
|
|
# required=True,
|
|
# order=2,
|
|
# is_predefined=True,
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=contact_stage,
|
|
# label="Phone Number",
|
|
# field_type="phone",
|
|
# required=True,
|
|
# order=3,
|
|
# is_predefined=True,
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=contact_stage,
|
|
# label="Address",
|
|
# field_type="text",
|
|
# required=False,
|
|
# order=4,
|
|
# is_predefined=True,
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=contact_stage,
|
|
# label="National ID / Iqama Number",
|
|
# field_type="text",
|
|
# required=False,
|
|
# order=5,
|
|
# is_predefined=True,
|
|
# )
|
|
FormField.objects.create(
|
|
stage=contact_stage,
|
|
label="GPA",
|
|
field_type="text",
|
|
required=False,
|
|
order=1,
|
|
is_predefined=True,
|
|
)
|
|
FormField.objects.create(
|
|
stage=contact_stage,
|
|
label="Resume Upload",
|
|
field_type="file",
|
|
required=True,
|
|
order=2,
|
|
is_predefined=True,
|
|
file_types=".pdf,.doc,.docx",
|
|
max_file_size=1,
|
|
)
|
|
|
|
# # Stage 2: Resume Objective
|
|
# objective_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Resume Objective',
|
|
# order=1,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=objective_stage,
|
|
# label='Career Objective',
|
|
# field_type='textarea',
|
|
# required=False,
|
|
# order=0,
|
|
# is_predefined=True
|
|
# )
|
|
|
|
# # Stage 3: Education
|
|
# education_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Education',
|
|
# order=2,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=education_stage,
|
|
# label='Degree',
|
|
# field_type='text',
|
|
# required=True,
|
|
# order=0,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=education_stage,
|
|
# label='Institution',
|
|
# field_type='text',
|
|
# required=True,
|
|
# order=1,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=education_stage,
|
|
# label='Location',
|
|
# field_type='text',
|
|
# required=False,
|
|
# order=2,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=education_stage,
|
|
# label='Graduation Date',
|
|
# field_type='date',
|
|
# required=False,
|
|
# order=3,
|
|
# is_predefined=True
|
|
# )
|
|
|
|
# # Stage 4: Experience
|
|
# experience_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Experience',
|
|
# order=3,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=experience_stage,
|
|
# label='Position Title',
|
|
# field_type='text',
|
|
# required=True,
|
|
# order=0,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=experience_stage,
|
|
# label='Company Name',
|
|
# field_type='text',
|
|
# required=True,
|
|
# order=1,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=experience_stage,
|
|
# label='Location',
|
|
# field_type='text',
|
|
# required=False,
|
|
# order=2,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=experience_stage,
|
|
# label='Start Date',
|
|
# field_type='date',
|
|
# required=True,
|
|
# order=3,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=experience_stage,
|
|
# label='End Date',
|
|
# field_type='date',
|
|
# required=True,
|
|
# order=4,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=experience_stage,
|
|
# label='Responsibilities & Achievements',
|
|
# field_type='textarea',
|
|
# required=False,
|
|
# order=5,
|
|
# is_predefined=True
|
|
# )
|
|
|
|
# # Stage 5: Skills
|
|
# skills_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Skills',
|
|
# order=4,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=skills_stage,
|
|
# label='Technical Skills',
|
|
# field_type='checkbox',
|
|
# required=False,
|
|
# order=0,
|
|
# is_predefined=True,
|
|
# options=['Programming Languages', 'Frameworks', 'Tools & Technologies']
|
|
# )
|
|
|
|
# # Stage 6: Summary
|
|
# summary_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Summary',
|
|
# order=5,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=summary_stage,
|
|
# label='Professional Summary',
|
|
# field_type='textarea',
|
|
# required=False,
|
|
# order=0,
|
|
# is_predefined=True
|
|
# )
|
|
|
|
# # Stage 7: Certifications
|
|
# certifications_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Certifications',
|
|
# order=6,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=certifications_stage,
|
|
# label='Certification Name',
|
|
# field_type='text',
|
|
# required=False,
|
|
# order=0,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=certifications_stage,
|
|
# label='Issuing Organization',
|
|
# field_type='text',
|
|
# required=False,
|
|
# order=1,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=certifications_stage,
|
|
# label='Issue Date',
|
|
# field_type='date',
|
|
# required=False,
|
|
# order=2,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=certifications_stage,
|
|
# label='Expiration Date',
|
|
# field_type='date',
|
|
# required=False,
|
|
# order=3,
|
|
# is_predefined=True
|
|
# )
|
|
|
|
# # Stage 8: Awards and Recognitions
|
|
# awards_stage = FormStage.objects.create(
|
|
# template=instance,
|
|
# name='Awards and Recognitions',
|
|
# order=7,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=awards_stage,
|
|
# label='Award Name',
|
|
# field_type='text',
|
|
# required=False,
|
|
# order=0,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=awards_stage,
|
|
# label='Issuing Organization',
|
|
# field_type='text',
|
|
# required=False,
|
|
# order=1,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=awards_stage,
|
|
# label='Date Received',
|
|
# field_type='date',
|
|
# required=False,
|
|
# order=2,
|
|
# is_predefined=True
|
|
# )
|
|
# FormField.objects.create(
|
|
# stage=awards_stage,
|
|
# label='Description',
|
|
# field_type='textarea',
|
|
# required=False,
|
|
# order=3,
|
|
# is_predefined=True
|
|
# )
|
|
|
|
|
|
# AgencyMessage signal handler removed - model has been deleted
|
|
|
|
# SSE notification cache for real-time updates
|
|
SSE_NOTIFICATION_CACHE = {}
|
|
|
|
|
|
@receiver(post_save, sender=Notification)
|
|
def notification_created(sender, instance, created, **kwargs):
|
|
"""Signal handler for when a notification is created"""
|
|
if created:
|
|
logger.info(
|
|
f"New notification created: {instance.id} for user {instance.recipient.username}"
|
|
)
|
|
|
|
# Store notification in cache for SSE
|
|
user_id = instance.recipient.id
|
|
if user_id not in SSE_NOTIFICATION_CACHE:
|
|
SSE_NOTIFICATION_CACHE[user_id] = []
|
|
|
|
notification_data = {
|
|
"id": instance.id,
|
|
"message": instance.message[:100]
|
|
+ ("..." if len(instance.message) > 100 else ""),
|
|
"type": instance.get_notification_type_display(),
|
|
"status": instance.get_status_display(),
|
|
"time_ago": "Just now",
|
|
"url": f"/notifications/{instance.id}/",
|
|
}
|
|
|
|
SSE_NOTIFICATION_CACHE[user_id].append(notification_data)
|
|
|
|
# Keep only last 50 notifications per user in cache
|
|
if len(SSE_NOTIFICATION_CACHE[user_id]) > 50:
|
|
SSE_NOTIFICATION_CACHE[user_id] = SSE_NOTIFICATION_CACHE[user_id][-50:]
|
|
|
|
logger.info(f"Notification cached for SSE: {notification_data}")
|
|
|
|
|
|
|
|
from .utils import generate_random_password
|
|
@receiver(post_save, sender=HiringAgency)
|
|
def hiring_agency_created(sender, instance, created, **kwargs):
|
|
if created:
|
|
logger.info(f"New hiring agency created: {instance.pk} - {instance.name}")
|
|
password = generate_random_password()
|
|
user = User.objects.create_user(
|
|
username=instance.name, email=instance.email, user_type="agency"
|
|
)
|
|
user.set_password(password)
|
|
user.save()
|
|
instance.user = user
|
|
instance.generated_password = password
|
|
instance.save()
|
|
logger.info(f"Generated password stored for agency: {instance.pk}")
|
|
|
|
|
|
@receiver(post_save, sender=Person)
|
|
def person_created(sender, instance, created, **kwargs):
|
|
if created and not instance.user:
|
|
logger.info(f"New Person created: {instance.pk} - {instance.email}")
|
|
try:
|
|
user = User.objects.create_user(
|
|
username=instance.email,
|
|
first_name=instance.first_name,
|
|
last_name=instance.last_name,
|
|
email=instance.email,
|
|
phone=instance.phone,
|
|
user_type="candidate",
|
|
)
|
|
instance.user = user
|
|
instance.save()
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
|
|
@receiver(post_save, sender=Source)
|
|
def source_created(sender, instance, created, **kwargs):
|
|
"""
|
|
Automatically generate API key and API secret when a new Source is created.
|
|
"""
|
|
if created:
|
|
# Only generate keys if they don't already exist
|
|
if not instance.api_key and not instance.api_secret:
|
|
logger.info(f"Generating API keys for new Source: {instance.pk} - {instance.name}")
|
|
|
|
# Generate API key and secret using existing secure functions
|
|
api_key = generate_api_key()
|
|
api_secret = generate_api_secret()
|
|
|
|
# Update the source with generated keys
|
|
instance.api_key = api_key
|
|
instance.api_secret = api_secret
|
|
instance.save(update_fields=['api_key', 'api_secret'])
|
|
|
|
logger.info(f"API keys generated successfully for Source: {instance.name} (Key: {api_key[:8]}...)")
|
|
else:
|
|
logger.info(f"Source {instance.name} already has API keys, skipping generation")
|