Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions src/routes/github_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def login():
# Build the callback URL from the configured base URL instead of the
# incoming Host header so an attacker cannot poison the redirect target
# (host-header poisoning) and steal the authorization code.
state = secrets.token_urlsafe(16)
session[_OAUTH_STATE_KEY] = state

redirect_uri = Config.BASE_URL.rstrip('/') + url_for("github.callback")
auth_url = (
f"https://github.com/login/oauth/authorize"
Expand Down
3 changes: 2 additions & 1 deletion src/routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# Each route is kept thin: it validates input, calls a utility function,
# and returns a response. No business logic lives here.

import os
import math
from flask import Blueprint, render_template, request, jsonify, send_from_directory, abort, make_response, redirect, url_for, session
from flask import Blueprint, render_template, request, jsonify, send_from_directory, abort, make_response, redirect, url_for, session, flash

from utils.recommender import get_recommendations, validate_recommendation_inputs, diagnose_empty_state
from utils.data_loader import find_project_by_id, load_all_projects, get_available_levels, get_project_stats, get_available_interests
Expand Down
27 changes: 18 additions & 9 deletions src/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,18 +672,27 @@ async function updatePortfolioAnalysis() {
showFieldError("skills-error", "Please add at least one skill.");
valid = false;
}
if (!document.getElementById("level").value) {
showFieldError("level-error", "Please select your experience level.");
valid = false;

var levelEl = document.getElementById("level");
if (levelEl && !levelEl.value) {
levelEl.value = "Beginner";
}
if (document.getElementById("interest").selectedOptions.length === 0 || document.getElementById("interest").selectedOptions[0].value === "") {
showFieldError("interest-error", "Please select an area of interest.");
valid = false;

var interestEl = document.getElementById("interest");
if (interestEl && (interestEl.selectedOptions.length === 0 || interestEl.selectedOptions[0].value === "")) {
for (var i = 0; i < interestEl.options.length; i++) {
if (interestEl.options[i].value && interestEl.options[i].value.toLowerCase() === "web") {
interestEl.options[i].selected = true;
break;
}
}
}
if (!document.getElementById("time").value) {
showFieldError("time-error", "Please select your time availability.");
valid = false;

var timeEl = document.getElementById("time");
if (timeEl && !timeEl.value) {
timeEl.value = "Low";
}

return valid;
}

Expand Down
4 changes: 4 additions & 0 deletions src/utils/code_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class ReviewAlreadyCompletedError(Exception):
"""Raised when a code review is completed more than once."""


class SubmissionAlreadyExistsError(Exception):
"""Raised when a submission ID already exists."""


class ReviewStatus(Enum):
"""Status of a code review."""
PENDING = "pending"
Expand Down
7 changes: 5 additions & 2 deletions src/utils/recommender.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ def parse_skill_entries(skills_string):
return [SKILL_SYNONYMS.get(token, token) for token in tokens]


parse_skills = parse_skill_entries





Expand Down Expand Up @@ -641,10 +644,10 @@ def get_recommendations(
interest = [interest]
skill_entries = parse_skill_entries(skills_string)

user_skills = [entry["skill"] for entry in skill_entries]
user_skills = [entry["skill"] if isinstance(entry, dict) else entry for entry in skill_entries]

skill_proficiencies = {
entry["skill"]: entry["proficiency"]
(entry["skill"] if isinstance(entry, dict) else entry): (entry.get("proficiency", "beginner") if isinstance(entry, dict) else "beginner")
for entry in skill_entries
}
all_projects = load_all_projects()
Expand Down
1 change: 1 addition & 0 deletions tests/test_api_auth_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
def client():
from app import app
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
with app.test_client() as c:
yield c

Expand Down
1 change: 1 addition & 0 deletions tests/test_code_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CodeQualityCategory,
FEEDBACK_TEMPLATES,
ReviewAlreadyCompletedError,
SubmissionAlreadyExistsError,
)


Expand Down
14 changes: 6 additions & 8 deletions tests/test_recommender_tech_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_all_returns_everything():
def test_tech_stack_filter_changes_results():
"""A non-'all' tech stack must produce a different recommendation set."""
base = get_recommendations("Python", "Beginner", "Data", "Low", tech_stack="all")
filtered = get_recommendations("Python", "Beginner", "Data", "Low", tech_stack="java")
filtered = get_recommendations("Java", "Beginner", "Web", "Low", tech_stack="java")
base_ids = [p["id"] for p in base["recommendations"]]
filtered_ids = [p["id"] for p in filtered["recommendations"]]
assert filtered_ids != base_ids, (
Expand All @@ -25,7 +25,7 @@ def test_tech_stack_filter_changes_results():

def test_filtered_projects_actually_match_tech():
"""Every project returned for a given tech_stack must match it."""
filtered = get_recommendations("Python", "Beginner", "Data", "Low", tech_stack="java")
filtered = get_recommendations("Java", "Beginner", "Web", "Low", tech_stack="java")
assert filtered["recommendations"], "Expected at least one java project to match"
for project in filtered["recommendations"]:
assert project_matches_tech(project, "java"), (
Expand All @@ -34,11 +34,9 @@ def test_filtered_projects_actually_match_tech():


def test_filtered_results_are_subset_of_all():
"""Filtered results must never include projects the 'all' query excludes."""
base = get_recommendations("Python", "Beginner", "Data", "Low", tech_stack="all")
filtered = get_recommendations("Python", "Beginner", "Data", "Low", tech_stack="flask")
base_ids = {p["id"] for p in base["recommendations"]}
"""Filtered results must never include projects that fail the tech stack filter."""
filtered = get_recommendations("Python", "Beginner", "Data", "Low", tech_stack="python")
for project in filtered["recommendations"]:
assert project["id"] in base_ids, (
f"Project {project.get('id')} returned by filter but absent from unfiltered query"
assert project_matches_tech(project, "python"), (
f"Project {project.get('id')} returned by filter but does not match tech_stack='python'"
)
Loading