Spaces:
Sleeping
Sleeping
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| return render_template('index.html') | |
| 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) | |
| 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') | |
| 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) | |
| 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]) | |
| 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) | |