diff --git a/src/routes/github_routes.py b/src/routes/github_routes.py index b88a0a4c..085f43df 100644 --- a/src/routes/github_routes.py +++ b/src/routes/github_routes.py @@ -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" diff --git a/src/routes/main_routes.py b/src/routes/main_routes.py index d5b8cdf4..456e0f5c 100644 --- a/src/routes/main_routes.py +++ b/src/routes/main_routes.py @@ -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 diff --git a/src/static/script.js b/src/static/script.js index 64989919..a50ac378 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -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; } diff --git a/src/utils/code_review.py b/src/utils/code_review.py index 5f3cdbb4..1625fbd0 100644 --- a/src/utils/code_review.py +++ b/src/utils/code_review.py @@ -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" diff --git a/src/utils/recommender.py b/src/utils/recommender.py index b1245c88..8304c359 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -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 + + @@ -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() diff --git a/tests/test_api_auth_required.py b/tests/test_api_auth_required.py index b2630303..2bfb5536 100644 --- a/tests/test_api_auth_required.py +++ b/tests/test_api_auth_required.py @@ -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 diff --git a/tests/test_code_review.py b/tests/test_code_review.py index 3535feab..454c3022 100644 --- a/tests/test_code_review.py +++ b/tests/test_code_review.py @@ -9,6 +9,7 @@ CodeQualityCategory, FEEDBACK_TEMPLATES, ReviewAlreadyCompletedError, + SubmissionAlreadyExistsError, ) diff --git a/tests/test_recommender_tech_stack.py b/tests/test_recommender_tech_stack.py index 70559837..880dbd5a 100644 --- a/tests/test_recommender_tech_stack.py +++ b/tests/test_recommender_tech_stack.py @@ -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, ( @@ -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"), ( @@ -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'" )