""" Utility functions for recruitment app """ from recruitment import models from django.conf import settings from datetime import datetime, timedelta, time, date from django.utils import timezone from .models import ScheduledInterview from django.template.loader import render_to_string from django.core.mail import send_mail import os import json import logging import requests from PyPDF2 import PdfReader from django.conf import settings from .models import Settings, Application logger = logging.getLogger(__name__) def get_setting(key, default=None): """ 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) ##################################### def extract_text_from_pdf(file_path): print("text extraction") text = "" try: with open(file_path, "rb") as f: reader = PdfReader(f) for page in reader.pages: text += (page.extract_text() or "") except Exception as e: logger.error(f"PDF extraction failed: {e}") raise return text.strip() def score_resume_with_openrouter(prompt): 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( url=OPENROUTER_API_URL, headers={ "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json", }, data=json.dumps({ "model": OPENROUTER_MODEL, "messages": [{"role": "user", "content": prompt}], }, ) ) # print(response.status_code) # print(response.json()) res = {} if response.status_code == 200: res = response.json() content = res["choices"][0]['message']['content'] try: content = content.replace("```json","").replace("```","") res = json.loads(content) except Exception as e: print(e) # res = raw_output["choices"][0]["message"]["content"] else: print("error response") return res # print(f"rawraw_output) # print(response) # def match_resume_with_job_description(resume, job_description,prompt=""): # resume_doc = nlp(resume) # job_doc = nlp(job_description) # similarity = resume_doc.similarity(job_doc) # return similarity def dashboard_callback(request, context): total_jobs = models.Job.objects.count() total_candidates = models.Candidate.objects.count() jobs = models.Job.objects.all() job_titles = [job.title for job in jobs] job_app_counts = [job.candidates.count() for job in jobs] context.update({ "total_jobs": total_jobs, "total_candidates": total_candidates, "job_titles": job_titles, "job_app_counts": job_app_counts, }) return context def get_access_token(): """Obtain an access token using server-to-server OAuth.""" 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_AUTH_URL = get_setting("ZOOM_AUTH_URL") headers = { "Content-Type": "application/x-www-form-urlencoded", } data = { "grant_type": "account_credentials", "account_id": ZOOM_ACCOUNT_ID, } auth = (ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET) response = requests.post(ZOOM_AUTH_URL, headers=headers, data=data, auth=auth) if response.status_code == 200: return response.json().get("access_token") else: raise Exception(f"Failed to obtain access token: {response.json()}") def create_zoom_meeting(topic, start_time, duration): """ Create a Zoom meeting using the Zoom API. Args: topic (str): The topic of the meeting. start_time (str): The start time of the meeting in ISO 8601 format (e.g., "2023-10-01T10:00:00Z"). duration (int): The duration of the meeting in minutes. Returns: dict: A dictionary containing the meeting details if successful, or an error message if failed. """ try: access_token = get_access_token() meeting_details = { "topic": topic, "type": 2, "start_time": start_time.isoformat() + "Z", "duration": duration, "timezone": "UTC", "settings": { "host_video": True, "participant_video": True, "join_before_host": True, "mute_upon_entry": False, "approval_type": 2, "audio": "both", "auto_recording": "none" } } # Make API request to Zoom to create the meeting headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } ZOOM_MEETING_URL = get_setting('ZOOM_MEETING_URL') response = requests.post( ZOOM_MEETING_URL, headers=headers, json=meeting_details ) # Check response status if response.status_code == 201: meeting_data = response.json() return { "status": "success", "message": "Meeting created successfully.", "meeting_details": { "join_url": meeting_data['join_url'], "meeting_id": meeting_data['id'], "password": meeting_data['password'], "host_email": meeting_data['host_email'] }, "zoom_gateway_response": meeting_data } else: return { "status": "error", "message": "Failed to create meeting.", "details": response.json() } except Exception as e: return { "status": "error", "message": str(e) } def list_zoom_meetings(next_page_token=None): """ List all meetings for a user using the Zoom API. Args: next_page_token (str, optional): The token for paginated results. Defaults to None. Returns: dict: A dictionary containing the list of meetings or an error message. """ try: access_token = get_access_token() user_id = 'me' params = {} if next_page_token: params['next_page_token'] = next_page_token headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } response = requests.get( f"https://api.zoom.us/v2/users/{user_id}/meetings", headers=headers, params=params ) if response.status_code == 200: meetings_data = response.json() return { "status": "success", "message": "Meetings retrieved successfully.", "meetings": meetings_data.get("meetings", []), "next_page_token": meetings_data.get("next_page_token") } else: return { "status": "error", "message": "Failed to retrieve meetings.", "details": response.json() } except Exception as e: return { "status": "error", "message": str(e) } def get_zoom_meeting_details(meeting_id): """ Retrieve details of a specific meeting using the Zoom API. Args: meeting_id (str): The ID of the meeting to retrieve. Returns: dict: A dictionary containing the meeting details or an error message. Date/datetime fields in 'meeting_details' will be ISO format strings. """ try: access_token = get_access_token() headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } response = requests.get( f"https://api.zoom.us/v2/meetings/{meeting_id}", headers=headers ) if response.status_code == 200: meeting_data = response.json() datetime_fields = [ 'start_time', 'created_at', 'updated_at', 'password_changed_at', 'host_join_before_start_time', 'audio_recording_start', 'recording_files_end' # Add any other known datetime fields ] for field_name in datetime_fields: if field_name in meeting_data and meeting_data[field_name] is not None: try: # Convert ISO 8601 string to datetime object, then back to ISO string # This ensures consistent string format, handling 'Z' for UTC dt_obj = datetime.fromisoformat(meeting_data[field_name].replace('Z', '+00:00')) meeting_data[field_name] = dt_obj.isoformat() except (ValueError, TypeError) as e: logger.warning( f"Could not parse or re-serialize datetime field '{field_name}' " f"for meeting {meeting_id}: {e}. Original value: '{meeting_data[field_name]}'" ) # Keep original string if re-serialization fails, or set to None # meeting_data[field_name] = None return { "status": "success", "message": "Meeting details retrieved successfully.", "meeting_details": meeting_data } else: return { "status": "error", "message": "Failed to retrieve meeting details.", "details": response.json() } except Exception as e: return { "status": "error", "message": str(e) } def update_zoom_meeting(meeting_id, updated_data): """ Update a Zoom meeting using the Zoom API. Args: meeting_id (str): The ID of the meeting to update. updated_data (dict): A dictionary containing the fields to update (e.g., topic, start_time, duration). Returns: dict: A dictionary containing the updated meeting details or an error message. """ try: access_token = get_access_token() headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } response = requests.patch( f"https://api.zoom.us/v2/meetings/{meeting_id}/", headers=headers, json=updated_data ) print(response.status_code) if response.status_code == 204: return { "status": "success", "message": "Meeting updated successfully." } else: print(response.json()) return { "status": "error", "message": "Failed to update meeting.", } except Exception as e: return { "status": "error", "message": str(e) } def delete_zoom_meeting(meeting_id): """ Delete a Zoom meeting using the Zoom API. Args: meeting_id (str): The ID of the meeting to delete. Returns: dict: A dictionary indicating success or failure. """ try: access_token = get_access_token() headers = { "Authorization": f"Bearer {access_token}" } response = requests.delete( f"https://api.zoom.us/v2/meetings/{meeting_id}", headers=headers ) if response.status_code == 204: return { "status": "success", "message": "Meeting deleted successfully." } else: return { "status": "error", "message": "Failed to delete meeting.", "details": response.json() } except Exception as e: return { "status": "error", "message": str(e) } def schedule_interviews(schedule): """ Schedule interviews for all candidates in the schedule based on the criteria. Returns the number of interviews successfully scheduled. """ candidates = list(schedule.candidates.all()) if not candidates: return 0 # Calculate available time slots available_slots = get_available_time_slots(schedule) if len(available_slots) < len(candidates): raise ValueError(f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}") # Schedule interviews scheduled_count = 0 for i, candidate in enumerate(candidates): slot = available_slots[i] interview_datetime = datetime.combine(slot['date'], slot['time']) # Create Zoom meeting meeting_topic = f"Interview for {schedule.job.title} - {candidate.name}" meeting = create_zoom_meeting( topic=meeting_topic, start_time=interview_datetime, duration=schedule.interview_duration, timezone=timezone.get_current_timezone_name() ) # Create scheduled interview record scheduled_interview = ScheduledInterview.objects.create( candidate=candidate, job=schedule.job, zoom_meeting=meeting, schedule=schedule, interview_date=slot['date'], interview_time=slot['time'] ) candidate.interview_date=interview_datetime # Send email to candidate send_interview_email(scheduled_interview) scheduled_count += 1 return scheduled_count def send_interview_email(scheduled_interview): """ Send an interview invitation email to the candidate. """ subject = f"Interview Invitation for {scheduled_interview.job.title}" context = { 'candidate_name': scheduled_interview.candidate.name, 'job_title': scheduled_interview.job.title, 'company_name': scheduled_interview.job.company.name, 'interview_date': scheduled_interview.interview_date, 'interview_time': scheduled_interview.interview_time, 'join_url': scheduled_interview.zoom_meeting.join_url, 'meeting_id': scheduled_interview.zoom_meeting.meeting_id, } # Render email templates text_message = render_to_string('interviews/email/interview_invitation.txt', context) html_message = render_to_string('interviews/email/interview_invitation.html', context) # Send email send_mail( subject=subject, message=text_message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[scheduled_interview.candidate.email], html_message=html_message, fail_silently=False, ) def get_available_time_slots(schedule): """ Generate a list of available time slots based on the schedule criteria. Returns a list of dictionaries with 'date' and 'time' keys. """ slots = [] current_date = schedule.start_date end_date = schedule.end_date # Convert working days to a set for quick lookup working_days_set = set(int(day) for day in schedule.working_days) # Parse times start_time = schedule.start_time end_time = schedule.end_time # Calculate slot duration (interview duration + buffer time) slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time) # Get breaks from the schedule breaks = schedule.breaks if hasattr(schedule, 'breaks') and schedule.breaks else [] while current_date <= end_date: # Check if current day is a working day weekday = current_date.weekday() # Monday is 0, Sunday is 6 if weekday in working_days_set: # Generate slots for this day current_time = start_time while True: # Calculate the end time of this slot slot_end_time = (datetime.combine(current_date, current_time) + slot_duration).time() # Check if the slot fits within the working hours if slot_end_time > end_time: break # Check if slot conflicts with any break time conflict_with_break = False for break_data in breaks: # Parse break times try: break_start = datetime.strptime(break_data['start_time'], '%H:%M:%S').time() break_end = datetime.strptime(break_data['end_time'], '%H:%M:%S').time() # Check if the slot overlaps with this break time if not (current_time >= break_end or slot_end_time <= break_start): conflict_with_break = True break except (ValueError, KeyError) as e: continue if not conflict_with_break: # Add this slot to available slots slots.append({ 'date': current_date, 'time': current_time }) # Move to next slot current_datetime = datetime.combine(current_date, current_time) + slot_duration current_time = current_datetime.time() # Move to next day current_date += timedelta(days=1) return slots def json_to_markdown_table(data_list): if not data_list: return "" headers = data_list[0].keys() markdown = "| " + " | ".join(headers) + " |\n" markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n" for row in data_list: values = [str(row.get(header, "")) for header in headers] markdown += "| " + " | ".join(values) + " |\n" return markdown def get_applications_from_request(request): for c in request.POST.items(): try: yield models.Application.objects.get(pk=c[0]) except Exception as e: logger.error(e) yield None def update_meeting(instance, updated_data): result = update_zoom_meeting(instance.meeting_id, updated_data) if result["status"] == "success": details_result = get_zoom_meeting_details(instance.meeting_id) if details_result["status"] == "success": zoom_details = details_result["meeting_details"] instance.topic = zoom_details.get("topic", instance.topic) instance.duration = zoom_details.get("duration", instance.duration) instance.details_url = zoom_details.get("join_url", instance.details_url) instance.password = zoom_details.get("password", instance.password) instance.status = zoom_details.get("status") instance.zoom_gateway_response = details_result.get("meeting_details") # Store full response instance.save() logger.info(f"Successfully updated Zoom meeting {instance.meeting_id}.") return {"status": "success", "message": "Zoom meeting updated successfully."} elif details_result["status"] == "error": # If fetching details fails, save with form data and log a warning logger.warning( f"Successfully updated Zoom meeting {instance.meeting_id}, but failed to fetch updated details. " f"Error: {details_result.get('message', 'Unknown error')}" ) return {"status": "success", "message": "Zoom meeting updated successfully."} logger.warning(f"Failed to update Zoom meeting {instance.meeting_id}. Error: {result.get('message', 'Unknown error')}") return {"status": "error", "message": result.get("message", "Zoom meeting update failed.")} def generate_random_password(): import string return "".join(random.choices(string.ascii_letters + string.digits, k=12))