from rest_framework import generics, permissions, status, views from rest_framework.response import Response from .models import InterviewSession from .serializers import InterviewSessionSerializer from django.utils import timezone class InterviewSessionListCreateView(generics.ListCreateAPIView): serializer_class = InterviewSessionSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): company = self.request.company if not company: return InterviewSession.objects.none() queryset = InterviewSession.objects.filter(company=company) # Filter by creator/recruiter if user is a recruiter and not a company admin from companies.models import RecruiterProfile, CompanyAdminProfile is_admin = CompanyAdminProfile.objects.filter(user=self.request.user).exists() is_recruiter = RecruiterProfile.objects.filter(user=self.request.user).exists() if is_recruiter and not is_admin: queryset = queryset.filter(job__created_by=self.request.user) return queryset.order_by('-scheduled_at') class InterviewSessionDetailView(views.APIView): # AllowAny for get and patch so candidate and recruiter can access details / reschedule permission_classes = [permissions.AllowAny] def get(self, request, room_id): try: session = InterviewSession.objects.get(room_id=room_id) except InterviewSession.DoesNotExist: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) return Response(InterviewSessionSerializer(session).data, status=status.HTTP_200_OK) def patch(self, request, room_id): try: session = InterviewSession.objects.get(room_id=room_id) except InterviewSession.DoesNotExist: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) scheduled_at = request.data.get('scheduled_at') if not scheduled_at: return Response({"scheduled_at": ["This field is required."]}, status=status.HTTP_400_BAD_REQUEST) try: session.scheduled_at = scheduled_at session.save() return Response(InterviewSessionSerializer(session).data, status=status.HTTP_200_OK) except Exception as e: return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) class UpdatePrivateNotesView(views.APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request, room_id): company = request.company session = InterviewSession.objects.filter(company=company, room_id=room_id).first() if not session: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) notes = request.data.get('notes', '') session.private_notes = notes session.save() return Response(InterviewSessionSerializer(session).data, status=status.HTTP_200_OK) class SubmitFeedbackView(views.APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request, room_id): company = request.company session = InterviewSession.objects.filter(company=company, room_id=room_id).first() if not session: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) feedback_data = request.data.get('feedback') if not feedback_data: return Response({"feedback": ["This field is required."]}, status=status.HTTP_400_BAD_REQUEST) # Validate feedback structure required_fields = ["technical_skills", "communication", "problem_solving", "culture_fit", "recommendation"] for field in required_fields: if field not in feedback_data: return Response({"feedback": [f"Missing required scorecard field: '{field}'"]}, status=status.HTTP_400_BAD_REQUEST) session.feedback = feedback_data session.completed_at = timezone.now() session.save() # Update candidate application status from candidates.models import Application application = Application.objects.filter(job=session.job, candidate=session.candidate).first() if application: rec = feedback_data.get('recommendation') if rec in ('Strong Yes', 'Yes'): application.current_stage = "Offer" application.status = "Offered" elif rec in ('No', 'Strong No'): application.current_stage = "Rejected" application.status = "Rejected" application.save() return Response({ "message": "Structured feedback submitted successfully.", "session": InterviewSessionSerializer(session).data }, status=status.HTTP_200_OK) class CandidateMyInterviewsView(views.APIView): permission_classes = [permissions.AllowAny] def get(self, request): email = request.query_params.get('email') if not email: return Response({"detail": "email parameter is required."}, status=status.HTTP_400_BAD_REQUEST) sessions = InterviewSession.objects.filter(candidate__email=email).order_by('-scheduled_at') serializer = InterviewSessionSerializer(sessions, many=True) return Response(serializer.data, status=status.HTTP_200_OK) import base64 import requests import json import uuid import os import time import logging import re import random from html.parser import HTMLParser from html import unescape from django.conf import settings from rest_framework.parsers import MultiPartParser, FormParser from assessments.models import CodingQuestion import jwt import pdfplumber logger = logging.getLogger(__name__) class CodeforcesProblemParser(HTMLParser): def __init__(self): super().__init__(convert_charrefs=False) self.in_statement = False self.statement_depth = 0 self.current_section = None self.current_section_depth = 0 self.title = '' self.time_limit = '' self.memory_limit = '' self.statement_parts = [] self.input_parts = [] self.output_parts = [] self.sample_inputs = [] self.sample_outputs = [] self._capture_stack = [] def _has_class(self, attrs, class_name): classes = '' for key, value in attrs: if key == 'class': classes = value or '' break return class_name in classes.split() def _start_capture(self, name): self._capture_stack.append({ 'name': name, 'depth': 1, 'parts': [] }) def _append_to_capture(self, text): if self._capture_stack and text: self._capture_stack[-1]['parts'].append(text) def _finish_capture(self): capture = self._capture_stack.pop() text = self._normalize_text(''.join(capture['parts'])) name = capture['name'] if name == 'title': self.title = text elif name == 'time-limit': self.time_limit = re.sub(r'^time limit per test\s*', '', text, flags=re.I) elif name == 'memory-limit': self.memory_limit = re.sub(r'^memory limit per test\s*', '', text, flags=re.I) elif name == 'input': text = re.sub(r'^input\s*\n?', '', text, flags=re.I).strip() if text: self.sample_inputs.append(text) elif name == 'output': text = re.sub(r'^output\s*\n?', '', text, flags=re.I).strip() if text: self.sample_outputs.append(text) def _append_section_text(self, text): if not self.in_statement or self._capture_stack: return if self.current_section == 'input-specification': self.input_parts.append(text) elif self.current_section == 'output-specification': self.output_parts.append(text) elif self.current_section not in ('sample-tests',): self.statement_parts.append(text) def handle_starttag(self, tag, attrs): if self._capture_stack: self._capture_stack[-1]['depth'] += 1 if tag in ('br', 'pre'): self._append_to_capture('\n') return if self._has_class(attrs, 'problem-statement'): self.in_statement = True self.statement_depth = 1 return if self.in_statement: self.statement_depth += 1 section_names = [ 'title', 'time-limit', 'memory-limit', 'input-specification', 'output-specification', 'sample-tests' ] for name in section_names: if self._has_class(attrs, name): self.current_section = name self.current_section_depth = self.statement_depth if name in ('title', 'time-limit', 'memory-limit'): self._start_capture(name) return if self.current_section == 'sample-tests': if self._has_class(attrs, 'input'): self._start_capture('input') elif self._has_class(attrs, 'output'): self._start_capture('output') if tag in ('p', 'div') and self.in_statement: self._append_section_text('\n') elif tag == 'li' and self.in_statement: self._append_section_text('\n- ') elif tag == 'br' and self.in_statement: self._append_section_text('\n') def handle_endtag(self, tag): if self._capture_stack: if tag == 'pre': self._append_to_capture('\n') self._capture_stack[-1]['depth'] -= 1 if self._capture_stack[-1]['depth'] > 0: return self._finish_capture() if self.current_section and self.statement_depth <= self.current_section_depth: self.current_section = None self.current_section_depth = 0 if self.in_statement: self.statement_depth -= 1 if self.statement_depth <= 0: self.in_statement = False def handle_data(self, data): text = unescape(data) self._append_to_capture(text) self._append_section_text(text) def handle_entityref(self, name): self.handle_data(f'&{name};') def handle_charref(self, name): self.handle_data(f'&#{name};') def _normalize_text(self, text): text = unescape(text).replace('\xa0', ' ') text = re.sub(r'[ \t]+', ' ', text) text = re.sub(r' *\n *', '\n', text) text = re.sub(r'\n{3,}', '\n\n', text) return text.strip() def parsed(self): statement = self._normalize_text(''.join(self.statement_parts)) input_spec = re.sub( r'^input\s*\n?', '', self._normalize_text(''.join(self.input_parts)), flags=re.I ).strip() output_spec = re.sub( r'^output\s*\n?', '', self._normalize_text(''.join(self.output_parts)), flags=re.I ).strip() samples = [] for sample_input, sample_output in zip(self.sample_inputs, self.sample_outputs): if sample_input and sample_output: samples.append({ "input": sample_input, "expected_output": sample_output, "is_hidden": False }) return { "title": self.title, "statement": statement, "input_spec": input_spec, "output_spec": output_spec, "time_limit": self.time_limit, "memory_limit": self.memory_limit, "test_cases": samples, } def fetch_codeforces_problem_details(problem_url, contest_id=None, problem_index=None): urls = [problem_url] if contest_id and problem_index: contest_url = f"https://codeforces.com/contest/{contest_id}/problem/{problem_index}" if contest_url not in urls: urls.append(contest_url) last_error = None headers = { "User-Agent": "Mozilla/5.0 (compatible; TalvexInterviewImporter/1.0)", "Accept-Language": "en-US,en;q=0.9", } for url in urls: try: response = requests.get(url, headers=headers, timeout=18) response.raise_for_status() parser = CodeforcesProblemParser() parser.feed(response.text) parsed = parser.parsed() if parsed.get('statement'): return parsed last_error = "problem statement markup was not found" except Exception as exc: last_error = exc if last_error: logger.warning(f"Failed to parse Codeforces problem page {problem_url}: {last_error}") return {} DEFAULT_CODING_QUESTIONS = [ { "id": 1, "title": "Two Sum", "difficulty": "Easy", "description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.\nYou may assume that each input would have exactly one solution, and you may not use the same element twice.\n\nExample 1:\nInput: nums = [2,7,11,15], target = 9\nOutput: [0,1]\nExplanation: nums[0] + nums[1] == 9, so we return [0, 1].", "starter_code": { "javascript": "function solution(nums, target) {\n // Write your code here\n return [];\n}", "python": "def solution(nums, target):\n # Write your code here\n return []" }, "test_cases": [ {"input": "[2,7,11,15], 9", "expected_output": "[0, 1]", "is_hidden": False} ] }, { "id": 2, "title": "Reverse Integer", "difficulty": "Medium", "description": "Given a signed 32-bit integer x, return x with its digits reversed. If reversing x causes the value to go outside the signed 32-bit integer range [-2^31, 2^31 - 1], then return 0.\n\nExample 1:\nInput: x = 123\nOutput: 321", "starter_code": { "javascript": "function solution(x) {\n // Write your code here\n return 0;\n}", "python": "def solution(x):\n # Write your code here\n return 0" }, "test_cases": [ {"input": "123", "expected_output": "321", "is_hidden": False} ] } ] def upload_pdf_to_supabase(uploaded_file, bucket_name='interview-questions'): ext = os.path.splitext(uploaded_file.name)[1] file_name = f"{uuid.uuid4()}{ext}" try: jwt_secret = settings.SUPABASE_JWT_SECRET decoded_secret = jwt_secret.encode('utf-8') if isinstance(jwt_secret, str) else jwt_secret payload = { "role": "service_role", "iss": "supabase", "iat": int(time.time()), "exp": int(time.time() + 3600) } token = jwt.encode(payload, decoded_secret, algorithm="HS256") if isinstance(token, bytes): token = token.decode('utf-8') headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } bucket_check_url = f"{settings.SUPABASE_URL}/storage/v1/bucket/{bucket_name}" check_res = requests.get(bucket_check_url, headers=headers) if check_res.status_code != 200: create_bucket_url = f"{settings.SUPABASE_URL}/storage/v1/bucket" body = {"id": bucket_name, "name": bucket_name, "public": True} requests.post(create_bucket_url, headers=headers, json=body) upload_headers = { "Authorization": f"Bearer {token}", } upload_url = f"{settings.SUPABASE_URL}/storage/v1/object/{bucket_name}/{file_name}" file_data = uploaded_file.read() content_type = "application/pdf" upload_headers["Content-Type"] = content_type res = requests.post(upload_url, headers=upload_headers, data=file_data) if res.status_code == 200: public_url = f"{settings.SUPABASE_URL}/storage/v1/object/public/{bucket_name}/{file_name}" return public_url except Exception as e: logger.error(f"Failed to upload PDF to Supabase: {e}") return None def extract_pdf_via_gemini_multimodal(file_bytes, model_name="gemini-1.5-flash"): api_key = os.environ.get('GEMINI_API_KEY') or os.environ.get('GOOGLE_API_KEY') if not api_key: logger.warning("No GEMINI_API_KEY or GOOGLE_API_KEY found.") return None url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}" base64_data = base64.b64encode(file_bytes).decode('utf-8') prompt = ( "You are an expert technical interviewer. Parse the provided coding interview question PDF " "(which may be typed or handwritten) and extract ALL coding questions into a single valid JSON array. " "Each question object MUST strictly have the following fields:\n" "- title: The name of the coding problem.\n" "- difficulty: Either 'Easy', 'Medium', or 'Hard'.\n" "- description: Clear and detailed description of the problem statement, parameters, and input/output rules.\n" "- starter_code: A JSON object with language starter templates, e.g. {\"javascript\": \"function solution() {\\n\\n}\", \"python\": \"def solution():\\n pass\"}.\n" "- test_cases: A JSON list of test case objects. E.g. [{\"input\": \"5\", \"expected_output\": \"10\", \"is_hidden\": false}].\n" "\nReturn ONLY the JSON array inside a raw text. Do not wrap in markdown or markdown code blocks like ```json." ) payload = { "contents": [{ "parts": [ {"text": prompt}, { "inlineData": { "mimeType": "application/pdf", "data": base64_data } } ] }] } try: response = requests.post(url, json=payload, timeout=20) if response.status_code == 200: res_json = response.json() content = res_json['candidates'][0]['content']['parts'][0]['text'] content = content.replace("```json", "").replace("```", "").strip() return json.loads(content) else: logger.error(f"Gemini {model_name} failed with status {response.status_code}: {response.text}") except Exception as e: logger.error(f"Error in Gemini multimodal parse: {e}") return None def parse_pdf_via_huggingface(raw_text): api_key = os.environ.get('HUGGINGFACE_API_KEY') or os.environ.get('HF_API_KEY') if not api_key: return None model_id = os.environ.get('HF_MODEL_ID', 'Qwen/Qwen2.5-7B-Instruct') url = f"https://api-inference.huggingface.co/models/{model_id}" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} prompt = ( f"Parse the following text from an interview questions document and extract the coding questions into a valid JSON array of question objects. " "Each question must contain: 'title', 'difficulty' ('Easy'/'Medium'/'Hard'), 'description', " "'starter_code' (object with 'javascript' and 'python'), and 'test_cases' (list of objects with 'input', 'expected_output', 'is_hidden'). " "Return ONLY valid JSON.\n\n" f"Document Text:\n{raw_text}" ) try: response = requests.post(url, json={"inputs": prompt, "parameters": {"max_new_tokens": 1000}}, headers=headers, timeout=12) if response.status_code == 200: res_data = response.json() content = "" if isinstance(res_data, list) and len(res_data) > 0: content = res_data[0].get('generated_text', '').strip() elif isinstance(res_data, dict): content = res_data.get('generated_text', '').strip() json_match = re.search(r'\[.*\]', content, re.DOTALL) if json_match: return json.loads(json_match.group(0)) except Exception as e: logger.error(f"Hugging Face PDF parsing fallback failed: {e}") return None def parse_pdf_heuristics(raw_text): questions = [] if not raw_text: raw_text = "Standard Coding Challenge" parts = re.split(r'(?:Question|Problem|Task)\s*\d*\s*[:\-]\s*', raw_text, flags=re.IGNORECASE) if len(parts) > 1: for i, part in enumerate(parts[1:]): lines = [l.strip() for l in part.split('\n') if l.strip()] title = lines[0] if lines else f"Coding Challenge {i+1}" desc = "\n".join(lines[1:]) if len(lines) > 1 else part questions.append({ "title": title[:100], "difficulty": "Medium", "description": desc, "starter_code": { "javascript": "function solution() {\n // Write your code here\n}", "python": "def solution():\n pass" }, "test_cases": [{"input": "1", "expected_output": "1", "is_hidden": False}] }) if not questions: questions.append({ "title": "Algorithm Challenge", "difficulty": "Medium", "description": raw_text[:2000], "starter_code": { "javascript": "function solution() {\n // Write your code here\n}", "python": "def solution():\n pass" }, "test_cases": [{"input": "1", "expected_output": "1", "is_hidden": False}] }) return questions class InterviewQuestionsView(views.APIView): permission_classes = [permissions.AllowAny] def get(self, request, room_id): try: session = InterviewSession.objects.get(room_id=room_id) except InterviewSession.DoesNotExist: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) if not session.questions: questions_qs = CodingQuestion.objects.filter(company__isnull=True) if questions_qs.exists(): session.questions = CodingQuestionSerializer(questions_qs, many=True).data else: session.questions = DEFAULT_CODING_QUESTIONS session.save() return Response(session.questions, status=status.HTTP_200_OK) def post(self, request, room_id): try: session = InterviewSession.objects.get(room_id=room_id) except InterviewSession.DoesNotExist: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) questions = request.data.get('questions') if not isinstance(questions, list): return Response({"detail": "questions field must be a list."}, status=status.HTTP_400_BAD_REQUEST) session.questions = questions session.save() return Response(session.questions, status=status.HTTP_200_OK) class InterviewImportPdfView(views.APIView): permission_classes = [permissions.AllowAny] parser_classes = [MultiPartParser, FormParser] def post(self, request, room_id): try: session = InterviewSession.objects.get(room_id=room_id) except InterviewSession.DoesNotExist: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) uploaded_file = request.FILES.get('file') if not uploaded_file: return Response({"error": "No PDF file uploaded."}, status=status.HTTP_400_BAD_REQUEST) file_bytes = uploaded_file.read() uploaded_file.seek(0) pdf_url = upload_pdf_to_supabase(uploaded_file) parsed_questions = None # Level 1: Gemini 1.5 Flash try: logger.info("Attempting PDF multimodal parse via Gemini 1.5 Flash...") parsed_questions = extract_pdf_via_gemini_multimodal(file_bytes, model_name="gemini-1.5-flash") except Exception as e: logger.error(f"Gemini 1.5 Flash failed: {e}") # Level 2: Gemini 1.5 Pro if not parsed_questions: try: logger.info("Attempting PDF multimodal parse via Gemini 1.5 Pro (Fallback)...") parsed_questions = extract_pdf_via_gemini_multimodal(file_bytes, model_name="gemini-1.5-pro") except Exception as e: logger.error(f"Gemini 1.5 Pro failed: {e}") # Level 3: Hugging Face Qwen-2.5 if not parsed_questions: try: logger.info("Attempting plain text PDF parse via Hugging Face...") raw_text = "" with pdfplumber.open(uploaded_file) as pdf: for page in pdf.pages: t = page.extract_text() if t: raw_text += t + "\n" parsed_questions = parse_pdf_via_huggingface(raw_text) except Exception as e: logger.error(f"Hugging Face plain text PDF parse failed: {e}") # Level 4: Heuristics if not parsed_questions: logger.warning("All LLM PDF parsers failed. Falling back to local heuristic segmenter.") try: uploaded_file.seek(0) raw_text = "" with pdfplumber.open(uploaded_file) as pdf: for page in pdf.pages: t = page.extract_text() if t: raw_text += t + "\n" parsed_questions = parse_pdf_heuristics(raw_text) except Exception as e: logger.error(f"Local heuristic PDF segmenter failed: {e}") parsed_questions = DEFAULT_CODING_QUESTIONS sanitized_questions = [] for idx, q in enumerate(parsed_questions): sanitized_questions.append({ "id": idx + 1, "title": q.get('title', f"Parsed Question {idx+1}"), "difficulty": q.get('difficulty', 'Medium'), "description": q.get('description', 'Solve the problem.'), "starter_code": q.get('starter_code', { "javascript": "function solution() {\n // Write your code here\n}", "python": "def solution():\n pass" }), "test_cases": q.get('test_cases', [ {"input": "1", "expected_output": "1", "is_hidden": False} ]) }) session.questions = sanitized_questions session.save() return Response({ "message": "PDF parsed successfully.", "questions": session.questions, "parsed_url": pdf_url }, status=status.HTTP_200_OK) class InterviewImportCodeforcesView(views.APIView): permission_classes = [permissions.AllowAny] def post(self, request, room_id): try: session = InterviewSession.objects.get(room_id=room_id) except InterviewSession.DoesNotExist: return Response({"detail": "Interview session not found."}, status=status.HTTP_404_NOT_FOUND) count = request.data.get('count', 5) try: count = int(count) except ValueError: count = 5 cf_url = "https://codeforces.com/api/problemset.problems" try: res = requests.get(cf_url, timeout=10) if res.status_code == 200: data = res.json() if data.get('status') == 'OK': problems = data.get('result', {}).get('problems', []) valid_problems = [ p for p in problems if p.get('rating') and 800 <= p.get('rating') <= 1500 ] if not valid_problems: valid_problems = problems sample_size = min(count, len(valid_problems)) selected_cf = random.sample(valid_problems, sample_size) imported_questions = [] for idx, p in enumerate(selected_cf): contest_id = p.get('contestId') index = p.get('index') name = p.get('name') rating = p.get('rating', 1000) difficulty = 'Easy' if rating <= 1100 else 'Medium' prob_link = f"https://codeforces.com/problemset/problem/{contest_id}/{index}" tags = ", ".join(p.get('tags', [])) details = fetch_codeforces_problem_details(prob_link, contest_id, index) statement = details.get('statement', '') input_spec = details.get('input_spec', '') output_spec = details.get('output_spec', '') sample_tests = details.get('test_cases') or [] time_limit = details.get('time_limit', '') memory_limit = details.get('memory_limit', '') metadata_lines = [ f"Codeforces Problem {contest_id}{index} - {name}", f"Link: {prob_link}", f"Difficulty Rating: {rating}", ] if tags: metadata_lines.append(f"Tags: {tags}") if time_limit: metadata_lines.append(f"Time Limit: {time_limit}") if memory_limit: metadata_lines.append(f"Memory Limit: {memory_limit}") if statement: desc_sections = [ "\n".join(metadata_lines), "Problem Statement:\n" + statement ] if input_spec: desc_sections.append("Input:\n" + input_spec) if output_spec: desc_sections.append("Output:\n" + output_spec) desc = "\n\n".join(desc_sections) else: desc = ( f"{chr(10).join(metadata_lines)}\n\n" f"Unable to fetch the full statement automatically. Please open the Codeforces link above for the complete problem.\n" f"Write a solution program that processes standard input correctly." ) imported_questions.append({ "id": idx + 1, "title": f"Codeforces: {name}", "difficulty": difficulty, "description": desc, "starter_code": { "javascript": f"// Codeforces problem: {prob_link}\nfunction solution(input) {{\n // Process input lines here\n}}", "python": f"# Codeforces problem: {prob_link}\ndef solution(input):\n # Process input lines here\n pass" }, "test_cases": sample_tests or [ {"input": "1", "expected_output": "1", "is_hidden": False} ] }) session.questions = imported_questions session.save() return Response(session.questions, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Codeforces import failed: {e}") return Response({"detail": "Failed to fetch from Codeforces API. Please try again later."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)