from flask import Flask, render_template, request, jsonify, redirect, url_for, flash from werkzeug.utils import secure_filename import os import json from utils.resume_parser import extract_text_from_pdf from utils.ai_analyzer import analyze_resume_with_jd from utils.scheduler import schedule_interview, send_rejection_email from utils.database import db, Candidate, JobDescription from datetime import datetime import logging logger = logging.getLogger(__name__) app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'ats-hackathon-secret') # HuggingFace Spaces: use absolute /app paths so gunicorn (non-root) can write BASE_DIR = os.path.dirname(os.path.abspath(__file__)) UPLOAD_DIR = os.path.join(BASE_DIR, 'uploads') DB_PATH = os.path.join(BASE_DIR, 'ats.db') os.makedirs(UPLOAD_DIR, exist_ok=True) app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['UPLOAD_FOLDER'] = UPLOAD_DIR app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max ALLOWED_EXTENSIONS = {'pdf'} db.init_app(app) with app.app_context(): db.create_all() def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS # ─── ROUTES ─────────────────────────────────────────────────────────────────── @app.route('/') def index(): return render_template('index.html') @app.route('/dashboard') def dashboard(): candidates = Candidate.query.order_by(Candidate.score.desc()).all() stats = { 'total': len(candidates), 'shortlisted': sum(1 for c in candidates if c.status == 'shortlisted'), 'rejected': sum(1 for c in candidates if c.status == 'rejected'), 'scheduled': sum(1 for c in candidates if c.status == 'scheduled'), 'avg_score': round(sum(c.score or 0 for c in candidates) / max(len(candidates), 1), 1) } return render_template('dashboard.html', candidates=candidates, stats=stats) @app.route('/upload', methods=['GET', 'POST']) def upload(): if request.method == 'POST': try: job_title = request.form.get('job_title', '') job_description = request.form.get('job_description', '') threshold = int(request.form.get('threshold', 70)) interview_date = request.form.get('interview_date', '') interview_time = request.form.get('interview_time', '') interview_link = request.form.get('interview_link', '') files = request.files.getlist('resumes') if not files or all(f.filename == '' for f in files): return jsonify({'success': False, 'error': 'No files received.'}), 400 results = [] for file in files: if not file or not allowed_file(file.filename): continue try: filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) resume_text = extract_text_from_pdf(filepath) if not resume_text.strip(): results.append({'name': filename, 'email': '', 'score': 0, 'status': 'error', 'id': None, 'error': 'Could not extract text from PDF'}) continue analysis = analyze_resume_with_jd(resume_text, job_description, job_title) if 'error' in analysis and not analysis.get('name'): results.append({'name': filename, 'email': '', 'score': 0, 'status': 'error', 'id': None, 'error': analysis['error']}) continue score = int(analysis.get('score', 0)) status = 'shortlisted' if score >= threshold else 'rejected' candidate = Candidate( name=analysis.get('name', 'Unknown'), email=analysis.get('email', ''), phone=analysis.get('phone', ''), score=score, status=status, job_title=job_title, skills_matched=json.dumps(analysis.get('matching_skills', [])), skills_missing=json.dumps(analysis.get('missing_skills', [])), reasoning=analysis.get('reasoning', ''), resume_filename=filename, created_at=datetime.utcnow() ) db.session.add(candidate) db.session.commit() if status == 'shortlisted' and analysis.get('email'): interview_datetime = (interview_date + ' ' + interview_time).strip() if interview_date else None email_sent = schedule_interview( candidate_email=analysis.get('email'), candidate_name=analysis.get('name'), job_title=job_title, interview_datetime=interview_datetime, interview_link=interview_link, candidate_id=candidate.id ) if email_sent: candidate.status = 'scheduled' candidate.interview_scheduled = interview_datetime db.session.commit() elif status == 'rejected' and analysis.get('email'): send_rejection_email( candidate_email=analysis.get('email'), candidate_name=analysis.get('name'), job_title=job_title ) results.append({ 'name': candidate.name, 'email': candidate.email, 'score': score, 'status': candidate.status, 'id': candidate.id }) except Exception as file_err: logger.error('Error processing file %s: %s', file.filename, file_err) results.append({'name': file.filename, 'email': '', 'score': 0, 'status': 'error', 'id': None, 'error': str(file_err)}) return jsonify({'success': True, 'results': results, 'total': len(results)}) except Exception as e: logger.error('Upload route error: %s', e) return jsonify({'success': False, 'error': str(e)}), 500 return render_template('upload.html') @app.route('/candidate/') def candidate_detail(id): candidate = Candidate.query.get_or_404(id) skills_matched = json.loads(candidate.skills_matched or '[]') skills_missing = json.loads(candidate.skills_missing or '[]') return render_template('candidate_detail.html', candidate=candidate, skills_matched=skills_matched, skills_missing=skills_missing) @app.route('/api/candidates') def api_candidates(): candidates = Candidate.query.order_by(Candidate.score.desc()).all() return jsonify([{ 'id': c.id, 'name': c.name, 'email': c.email, 'score': c.score, 'status': c.status, 'job_title': c.job_title, 'created_at': c.created_at.isoformat() if c.created_at else '' } for c in candidates]) @app.route('/api/reschedule/', methods=['POST']) def reschedule(id): candidate = Candidate.query.get_or_404(id) data = request.json email_sent = schedule_interview( candidate_email=candidate.email, candidate_name=candidate.name, job_title=candidate.job_title, interview_datetime=data.get('datetime'), interview_link=data.get('link'), candidate_id=id ) if email_sent: candidate.interview_scheduled = data.get('datetime') candidate.status = 'scheduled' db.session.commit() return jsonify({'success': True}) return jsonify({'success': False, 'error': 'Email failed'}) if __name__ == '__main__': os.makedirs('uploads', exist_ok=True) app.run(debug=True, port=5000)