kaauh_ats/recruitment/candidate_sync_service.py

363 lines
13 KiB
Python

import json
import logging
import requests
from datetime import datetime
from typing import Dict, Any, List, Optional, Tuple
from django.utils import timezone
from django.conf import settings
from django.core.files.base import ContentFile
from django.http import HttpRequest
from .models import Source, Candidate, JobPosting, IntegrationLog
logger = logging.getLogger(__name__)
class CandidateSyncService:
"""
Service to handle synchronization of hired candidates to external sources
"""
def __init__(self):
self.logger = logging.getLogger(__name__)
def sync_hired_candidates_to_all_sources(self, job: JobPosting) -> Dict[str, Any]:
"""
Sync all hired candidates for a job to all active external sources
Returns: Dictionary with sync results for each source
"""
results = {
'total_candidates': 0,
'successful_syncs': 0,
'failed_syncs': 0,
'source_results': {},
'sync_time': timezone.now().isoformat()
}
# Get all hired candidates for this job
hired_candidates = list(job.candidates.filter(
offer_status='Accepted'
).select_related('job'))
results['total_candidates'] = len(hired_candidates)
if not hired_candidates:
self.logger.info(f"No hired candidates found for job {job.title}")
return results
# Get all active sources that support outbound sync
active_sources = Source.objects.filter(
is_active=True,
sync_endpoint__isnull=False
).exclude(sync_endpoint='')
if not active_sources:
self.logger.warning("No active sources with sync endpoints configured")
return results
# Sync to each source
for source in active_sources:
try:
source_result = self.sync_to_source(source, hired_candidates, job)
results['source_results'][source.name] = source_result
if source_result['success']:
results['successful_syncs'] += 1
else:
results['failed_syncs'] += 1
except Exception as e:
error_msg = f"Unexpected error syncing to {source.name}: {str(e)}"
self.logger.error(error_msg)
results['source_results'][source.name] = {
'success': False,
'error': error_msg,
'candidates_synced': 0
}
results['failed_syncs'] += 1
return results
def sync_to_source(self, source: Source, candidates: List[Candidate], job: JobPosting) -> Dict[str, Any]:
"""
Sync candidates to a specific external source
Returns: Dictionary with sync result for this source
"""
result = {
'success': False,
'error': None,
'candidates_synced': 0,
'candidates_failed': 0,
'candidate_results': []
}
try:
# Prepare headers for the request
headers = self._prepare_headers(source)
# Sync each candidate
for candidate in candidates:
try:
candidate_data = self._format_candidate_data(candidate, job)
sync_result = self._send_candidate_to_source(source, candidate_data, headers)
result['candidate_results'].append({
'candidate_id': candidate.id,
'candidate_name': candidate.name,
'success': sync_result['success'],
'error': sync_result.get('error'),
'response_data': sync_result.get('response_data')
})
if sync_result['success']:
result['candidates_synced'] += 1
else:
result['candidates_failed'] += 1
except Exception as e:
error_msg = f"Error syncing candidate {candidate.name}: {str(e)}"
self.logger.error(error_msg)
result['candidate_results'].append({
'candidate_id': candidate.id,
'candidate_name': candidate.name,
'success': False,
'error': error_msg
})
result['candidates_failed'] += 1
# Consider sync successful if at least one candidate was synced
result['success'] = result['candidates_synced'] > 0
# Log the sync operation
self._log_sync_operation(source, result, len(candidates))
except Exception as e:
error_msg = f"Failed to sync to source {source.name}: {str(e)}"
self.logger.error(error_msg)
result['error'] = error_msg
return result
def _prepare_headers(self, source: Source) -> Dict[str, str]:
"""Prepare HTTP headers for the sync request"""
headers = {
'Content-Type': 'application/json',
'User-Agent': f'KAAUH-ATS-Sync/1.0'
}
# Add API key if configured
if source.api_key:
headers['X-API-Key'] = source.api_key
# Add custom headers if any
if hasattr(source, 'custom_headers') and source.custom_headers:
try:
custom_headers = json.loads(source.custom_headers)
headers.update(custom_headers)
except json.JSONDecodeError:
self.logger.warning(f"Invalid custom_headers JSON for source {source.name}")
return headers
def _format_candidate_data(self, candidate: Candidate, job: JobPosting) -> Dict[str, Any]:
"""Format candidate data for external source"""
data = {
'candidate': {
'id': candidate.id,
'slug': candidate.slug,
'first_name': candidate.first_name,
'last_name': candidate.last_name,
'full_name': candidate.name,
'email': candidate.email,
'phone': candidate.phone,
'address': candidate.address,
'applied_at': candidate.created_at.isoformat(),
'hired_date': candidate.offer_date.isoformat() if candidate.offer_date else None,
'join_date': candidate.join_date.isoformat() if candidate.join_date else None,
},
'job': {
'id': job.id,
'internal_job_id': job.internal_job_id,
'title': job.title,
'department': job.department,
'job_type': job.job_type,
'workplace_type': job.workplace_type,
'location': job.get_location_display(),
},
'ai_analysis': {
'match_score': candidate.match_score,
'years_of_experience': candidate.years_of_experience,
'screening_rating': candidate.screening_stage_rating,
'professional_category': candidate.professional_category,
'top_skills': candidate.top_3_keywords,
'strengths': candidate.strengths,
'weaknesses': candidate.weaknesses,
'recommendation': candidate.recommendation,
'job_fit_narrative': candidate.job_fit_narrative,
},
'sync_metadata': {
'synced_at': timezone.now().isoformat(),
'sync_source': 'KAAUH-ATS',
'sync_version': '1.0'
}
}
# Add resume information if available
if candidate.resume:
data['candidate']['resume'] = {
'filename': candidate.resume.name,
'size': candidate.resume.size,
'url': candidate.resume.url if hasattr(candidate.resume, 'url') else None
}
# Add additional AI analysis data if available
if candidate.ai_analysis_data:
data['ai_analysis']['full_analysis'] = candidate.ai_analysis_data
return data
def _send_candidate_to_source(self, source: Source, candidate_data: Dict[str, Any], headers: Dict[str, str]) -> Dict[str, Any]:
"""
Send candidate data to external source
Returns: Dictionary with send result
"""
result = {
'success': False,
'error': None,
'response_data': None,
'status_code': None
}
try:
# Determine HTTP method (default to POST)
method = getattr(source, 'sync_method', 'POST').upper()
# Prepare request data
json_data = json.dumps(candidate_data)
# Make the HTTP request
if method == 'POST':
response = requests.post(
source.sync_endpoint,
data=json_data,
headers=headers,
timeout=30
)
elif method == 'PUT':
response = requests.put(
source.sync_endpoint,
data=json_data,
headers=headers,
timeout=30
)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
result['status_code'] = response.status_code
result['response_data'] = response.text
# Check if request was successful
if response.status_code in [200, 201, 202]:
try:
response_json = response.json()
result['response_data'] = response_json
result['success'] = True
except json.JSONDecodeError:
# If response is not JSON, still consider it successful if status code is good
result['success'] = True
else:
result['error'] = f"HTTP {response.status_code}: {response.text}"
except requests.exceptions.Timeout:
result['error'] = "Request timeout"
except requests.exceptions.ConnectionError:
result['error'] = "Connection error"
except requests.exceptions.RequestException as e:
result['error'] = f"Request error: {str(e)}"
except Exception as e:
result['error'] = f"Unexpected error: {str(e)}"
return result
def _log_sync_operation(self, source: Source, result: Dict[str, Any], total_candidates: int):
"""Log the sync operation to IntegrationLog"""
try:
IntegrationLog.objects.create(
source=source,
action='SYNC',
endpoint=source.sync_endpoint,
method=getattr(source, 'sync_method', 'POST'),
request_data={
'total_candidates': total_candidates,
'candidates_synced': result['candidates_synced'],
'candidates_failed': result['candidates_failed']
},
response_data=result,
status_code='200' if result['success'] else '400',
error_message=result.get('error'),
ip_address='127.0.0.1', # Internal sync
user_agent='KAAUH-ATS-Sync/1.0'
)
except Exception as e:
self.logger.error(f"Failed to log sync operation: {str(e)}")
def test_source_connection(self, source: Source) -> Dict[str, Any]:
"""
Test connection to an external source
Returns: Dictionary with test result
"""
result = {
'success': False,
'error': None,
'response_time': None,
'status_code': None
}
try:
headers = self._prepare_headers(source)
test_data = {
'test': True,
'timestamp': timezone.now().isoformat(),
'source': 'KAAUH-ATS Connection Test'
}
start_time = datetime.now()
# Use GET method for testing if available, otherwise POST
test_method = getattr(source, 'test_method', 'GET').upper()
if test_method == 'GET':
response = requests.get(
source.sync_endpoint,
headers=headers,
timeout=10
)
else:
response = requests.post(
source.sync_endpoint,
data=json.dumps(test_data),
headers=headers,
timeout=10
)
end_time = datetime.now()
result['response_time'] = (end_time - start_time).total_seconds()
result['status_code'] = response.status_code
if response.status_code in [200, 201, 202]:
result['success'] = True
else:
result['error'] = f"HTTP {response.status_code}: {response.text}"
except requests.exceptions.Timeout:
result['error'] = "Connection timeout"
except requests.exceptions.ConnectionError:
result['error'] = "Connection failed"
except Exception as e:
result['error'] = f"Test failed: {str(e)}"
return result