Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:

- name: Test with pytest
run: |
pytest
pytest --ignore=tests/test_e2e.py
81 changes: 71 additions & 10 deletions be_repo/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
from flask import Flask, request, jsonify
from flask import Flask, request, jsonify, session
from flask_cors import CORS
from datetime import timedelta

from configs.database import get_resume_database
from modules.evaluator import evaluate_resume, evaluate_resume_with_jd
from modules.upload import upload_parse_resume
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
import secrets

# Generate a secure random secret key
secret_key = secrets.token_hex(32) # Generates a 64-character hexadecimal string

app = Flask(__name__)
CORS(app)
CORS(app, supports_credentials=True)
app.secret_key = secret_key

app.config.update(
SESSION_COOKIE_SECURE=False, # Set to True if using HTTPS
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
PERMANENT_SESSION_LIFETIME=timedelta(minutes=30),
)

GOOGLE_CLIENT_ID = '120137358324-l62fq2hlj9r31evvitg55rcl4rf21udd.apps.googleusercontent.com'

# Test MongoDB connection
try:
Expand All @@ -18,21 +35,62 @@
raise Exception("Unable to find the document due to the following error: ", e)


@app.route('/upload', methods=['POST'])
def upload_resume():
@app.route('/upload', methods=['POST', 'OPTIONS'])
def upload_resume():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

user_id = request.form.get('user_id')
if not user_id:
return jsonify({"error": "No user ID provided."}), 400
# Vector the resume text
return upload_parse_resume(request, resume_collection)


@app.route('/resume_evaluate', methods=['POST'])
@app.route('/login', methods=['POST', 'OPTIONS'])
def login():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

token = request.form.get('access_token')
if not token:
return jsonify({'status': 'error', 'message': 'ID token is missing'}), 400

try:
# Verify the token with Google
idinfo = id_token.verify_oauth2_token(token, google_requests.Request(), GOOGLE_CLIENT_ID)

if idinfo['aud'] != GOOGLE_CLIENT_ID:
return jsonify({'status': 'error', 'message': 'Token audience mismatch'}), 400

# Token is valid, extract user information
userid = idinfo['sub']
email = idinfo['email']
name = idinfo.get('name', 'No name available')

# Create a session or JWT for the user
session.permanent = True
session['user'] = {'userid': userid, 'email': email, 'name': name}

# Respond with success and optional user data
return jsonify({'status': 'success', 'email': email, 'name': name}), 200

except ValueError as e:
# Token verification failed
print(f"Error verifying token: {str(e)}")
return jsonify({'status': 'error', 'message': 'Invalid ID token'}), 400

@app.route('/resume_evaluate', methods=['POST', 'OPTIONS'])
def resume_evaluate():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

user_id = request.form.get('user_id')
if not user_id:
return jsonify({"error": "No user ID provided."}), 400

# Load resume from database
resume = resume_collection.find_one({"user_id": user_id})
if not resume:
Expand All @@ -47,11 +105,14 @@ def resume_evaluate():
return jsonify({"analysis": analysis_result}), 200


@app.route('/resume_evaluate_with_JD', methods=['POST'])
@app.route('/resume_evaluate_with_JD', methods=['POST', 'OPTIONS'])
def resume_evaluate_with_JD():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

user_id = request.form.get('user_id')
jd_text = request.form.get('jd_text')

if not user_id:
return jsonify({"error": "No user ID provided."}), 400
if not jd_text:
Expand All @@ -72,4 +133,4 @@ def resume_evaluate_with_JD():


if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
app.run(host='0.0.0.0', debug=True, port=5000)
7 changes: 5 additions & 2 deletions be_repo/modules/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ def extract_scores_and_explanation(evaluation):


def compute_correlated_score(score, correlation):
correlated_score = [score[i] * correlation[i] * 2 for i in range(4)]
max_ranges = [SCORING_CRITERIA['Education'], SCORING_CRITERIA['Project and Work Experience'],
SCORING_CRITERIA['Skills and Certifications'], SCORING_CRITERIA['Soft Skills']]
correlated_score = [min(int(score[i] * correlation[i] ** 2 * 2), max_ranges[i]) for i in range(4)]

return correlated_score


Expand Down Expand Up @@ -232,7 +235,7 @@ def evaluate_resume_with_jd(resume_text, jd_text):
# Extract correlation scores and original scores into arrays and compute correlated_score
correlation_array = list(correlation['correlation'].values())
scores_array = list(evaluated_resume_with_jd['scores'].values())
correlated_score = compute_correlated_score(correlation_array, scores_array)
correlated_score = compute_correlated_score(scores_array, correlation_array)

# Update the scores and explanations in the evaluated resume based on correlation analysis
fields_to_replace = ['Education', 'Project and Work Experience', 'Skills and Certifications', 'Soft Skills']
Expand Down
13 changes: 10 additions & 3 deletions be_repo/modules/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ def upload_parse_resume(request, resume_collection):
"user_id": user_id,
"resume_text": resume_text
}
result = resume_collection.insert_one(new_resume)
result = resume_collection.replace_one({"user_id": user_id}, new_resume, upsert=True)

if result.upserted_id:
resume_id = str(result.upserted_id)
message = "File successfully uploaded and parsed"
else:
resume_id = str(resume_collection.find_one({"user_id": user_id})["_id"])
message = "Existing resume updated successfully"

return jsonify({
"message": "File successfully uploaded and parsed",
"resume_id": str(result.inserted_id)
"message": message,
"resume_id": resume_id
}), 200
else:
return jsonify({"error": "Invalid file format, only PDF is allowed"}), 400
3 changes: 3 additions & 0 deletions be_repo/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ pymongo==4.10.1
PyPDF2==3.0.1
qdrant_client==1.12.0
tqdm==4.66.2
selenium==4.26.1
google-auth==2.36.0
google-auth-oauthlib==1.2.1
85 changes: 85 additions & 0 deletions be_repo/tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support import expected_conditions as EC
import time

FRONTEND_URL = "http://localhost:3001"
API_URL = "http://127.0.0.1:5001"

options = Options()
# options.add_argument("--headless")
driver = webdriver.Chrome(options=options)
driver.get(FRONTEND_URL)
wait = WebDriverWait(driver, 10)

try:
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "nsm7Bb-HzV7m-LgbsSe-MJoBVe"))
)

# Google login
google_login_button = driver.find_element(By.CLASS_NAME, "nsm7Bb-HzV7m-LgbsSe")
# google_login_button.click()

# Upload Resume
upload_div = driver.find_element(By.CSS_SELECTOR, "div[style*='cursor: pointer'][style*='display: flex']")
# upload_div.click()

wait = WebDriverWait(driver, 10)
file_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")))

file_path = "/Users/caozhen/PycharmProjects/seprojects-cs673a2f24_team5/be_repo/tests/test_resume.pdf"
file_input.send_keys(file_path)

alert = WebDriverWait(driver, 10).until(EC.alert_is_present())
alert_text = alert.text
assert "Resume uploaded successfully" in alert_text
alert.accept()

# Analyze resume
analyze_button = driver.find_element(By.CLASS_NAME, "cursor-pointer")
analyze_button.click()

submit_button = driver.find_element(By.CLASS_NAME, "mt-6")
submit_button.click()

time.sleep(20)

keywords = [
"Analysis Result",
"Consistency and Chronology",
"Education",
"Project and Work Experience",
"Resume Structure and Presentation",
"Skills and Certifications",
"Soft Skills"
]

elements = driver.find_elements(By.CLASS_NAME, "font-bold")
content_text = " ".join([element.text for element in elements])
for keyword in keywords:
count = content_text.count(keyword)
assert count == 1, f"'{keyword}' does not appear exactly twice in the content (found {count} times)"

# Analyze resume with JD
analyze_button.click()
textarea = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.w-full.h-40.p-4.border-2"))
)
textarea.send_keys("Sample job description text")
submit_button = driver.find_element(By.CLASS_NAME, "mt-6")
submit_button.click()

time.sleep(20)

elements = driver.find_elements(By.CLASS_NAME, "font-bold")
content_text = " ".join([element.text for element in elements])
for keyword in keywords:
count = content_text.count(keyword)
assert count == 2, f"'{keyword}' does not appear exactly twice in the content (found {count} times)"

finally:
driver.quit()
53 changes: 53 additions & 0 deletions be_repo/tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Flask Integration Test.
"""
import pytest
from flask import Flask
from app import app
from configs.database import get_resume_database


@pytest.fixture
def client():
# Create testing client
app.config['TESTING'] = True
app.config['DEBUG'] = False
client = app.test_client()

with app.app_context():
database = get_resume_database()
resume_collection = database.get_collection("resumes")
resume_collection.delete_many({"user_id": "test"})
yield client, resume_collection
resume_collection.delete_many({"user_id": "test"})


def test_resume_evaluate(client):
client, resume_collection = client

resume_collection.insert_one({
'user_id': 'test',
'resume_text': 'Sample resume content for evaluation.'
})

response = client.post('/resume_evaluate', data={'user_id': 'test'})
assert response.status_code == 200
json_data = response.get_json()
assert 'analysis' in json_data


def test_resume_evaluate_with_JD(client):
client, resume_collection = client

resume_collection.insert_one({
'user_id': 'test',
'resume_text': 'Sample resume content for JD evaluation.'
})

response = client.post('/resume_evaluate_with_JD', data={
'user_id': 'test',
'jd_text': 'Sample job description text for matching.'
})
assert response.status_code == 200
json_data = response.get_json()
assert 'analysis' in json_data
Binary file added be_repo/tests/test_resume.pdf
Binary file not shown.
5 changes: 0 additions & 5 deletions be_repo/tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ def test_upload_parse_resume_success(mock_parse_resume, app):
assert response[1] == 200
assert response[0].json['message'] == "File successfully uploaded and parsed"

mock_resume_collection.insert_one.assert_called_once_with({
"user_id": '123',
"resume_text": "Parsed resume text"
})


@patch('modules.upload.parse_resume', return_value="Parsed resume text")
def test_upload_parse_resume_invalid_format(mock_parse_resume, app):
Expand Down
Loading
Loading