361 lines
13 KiB
Python
361 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.hired_applications.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
|