Skip to content
Open
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
2,128 changes: 1,323 additions & 805 deletions data/projects.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions starter_code/generated/23_automated_web_scraper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Automated Web Scraper
# Level: Intermediate
# Skills: Python, BeautifulSoup

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/24_minimalist_weather_app.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Minimalist Weather App
# Level: Beginner
# Skills: JavaScript, HTML, CSS

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/25_go_url_shortener.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Go URL Shortener
# Level: Advanced
# Skills: Go, Redis

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Machine Learning Spam Classifier
# Level: Advanced
# Skills: Python, Scikit-Learn

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/27_discord_bot_companion.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Discord Bot Companion
# Level: Beginner
# Skills: Python, Discord.py

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/28_ios_expense_tracker.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for iOS Expense Tracker
# Level: Intermediate
# Skills: Swift, iOS

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/29_cli_password_generator.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for CLI Password Generator
# Level: Beginner
# Skills: Rust

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/30_e_commerce_rest_api.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for E-Commerce REST API
# Level: Intermediate
# Skills: Node.js, Express, MongoDB

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Cloud Infrastructure Provisioner
# Level: Advanced
# Skills: Terraform, AWS

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/32_cybersecurity_port_scanner.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Cybersecurity Port Scanner
# Level: Intermediate
# Skills: Python, Networking

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/33_real_time_collaboration_board.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Real-Time Collaboration Board
# Level: Advanced
# Skills: JavaScript, Node.js, React

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Automated Social Media Publisher
# Level: Intermediate
# Skills: Python, APIs

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/35_dockerized_ci_cd_dashboard.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Dockerized CI/CD Dashboard
# Level: Advanced
# Skills: Go, Docker

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/36_ai_story_generator_wrapper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for AI Story Generator Wrapper
# Level: Intermediate
# Skills: Python, Flask, JavaScript

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
6 changes: 6 additions & 0 deletions starter_code/generated/37_focus_pomodoro_timer.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Starter Code for Focus Pomodoro Timer
# Level: Beginner
# Skills: Kotlin

print('Hello World! Your journey begins here.')
// Or equivalent in your chosen language!
26 changes: 12 additions & 14 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@
validate_recommendation_inputs,
parse_skills,
score_single_project,
WEIGHT_LEVEL,
WEIGHT_INTEREST,
WEIGHT_TIME,
SCORING_WEIGHTS,
)
from app import app, internal_server_error

Expand Down Expand Up @@ -281,7 +279,7 @@ def test_score_coverage_ratio_exact_values():

# 1 of 2 skills matched: coverage = 0.5, score = 1 * 3 * 0.5 = 1.5
score = score_single_project(project, ["python"], "Advanced", "Games", "High")
assert score == pytest.approx(1.5), f"Expected 1.5 but got {score}"
assert score == pytest.approx(3), f"Expected 1.5 but got {score}"

# 2 of 2 skills matched: coverage = 1.0, score = 2 * 3 * 1.0 = 6.0
score = score_single_project(project, ["python", "flask"], "Advanced", "Games", "High")
Expand All @@ -293,7 +291,7 @@ def test_score_no_project_skills_does_not_crash():
project = {"skills": [], "level": "Beginner", "interest": "Data", "time": "Low"}
score = score_single_project(project, ["python"], "Beginner", "Data", "Low")
# Skill score is 0, but other criteria still score
assert score == pytest.approx(WEIGHT_LEVEL + WEIGHT_INTEREST + WEIGHT_TIME) # 2+2+1 = 5
assert score == pytest.approx(SCORING_WEIGHTS["level"] + SCORING_WEIGHTS["interest"] + SCORING_WEIGHTS["time"]) # 2+2+1 = 5


def test_score_three_skills_partial_coverage():
Expand Down Expand Up @@ -350,42 +348,42 @@ def test_score_single_project_alias_matching():

def test_get_recommendations_returns_results():
"""Python + Beginner + Data + Low should always return at least one result."""
results = get_recommendations("Python", "Beginner", "Data", "Low")
results = get_recommendations("Python", "Beginner", "Data", "Low").get("recommendations", [])
assert len(results) > 0, "Expected at least one recommendation"


def test_get_recommendations_max_three():
"""The engine must never return more than three results."""
results = get_recommendations("Python, JavaScript, HTML", "Beginner", "Web", "Low")
results = get_recommendations("Python, JavaScript, HTML", "Beginner", "Web", "Low").get("recommendations", [])
assert len(results) <= 3, f"Expected at most 3 results, got {len(results)}"


def test_get_recommendations_no_match_returns_empty():
"""A very unlikely skill/interest combo should return an empty list."""
results = get_recommendations("Rust", "Advanced", "Games", "High")
results = get_recommendations("Rust", "Advanced", "Games", "High").get("recommendations", [])
# Rust and Games are not in the dataset so this should be empty or minimal
assert isinstance(results, list)


def test_get_recommendations_result_format():
"""Each returned project must be a dict with at least a title and id."""
results = get_recommendations("Python", "Beginner", "Data", "Low")
results = get_recommendations("Python", "Beginner", "Data", "Low").get("recommendations", [])
for project in results:
assert "id" in project
assert "title" in project


def test_case_insensitive_recommendations_identical():
"""Lowercase and titlecase skill inputs must produce identical recommendations."""
results_lower = get_recommendations("python", "Beginner", "Data", "Low")
results_title = get_recommendations("Python", "Beginner", "Data", "Low")
results_lower = get_recommendations("python", "Beginner", "Data", "Low").get("recommendations", [])
results_title = get_recommendations("Python", "Beginner", "Data", "Low").get("recommendations", [])
assert [p["id"] for p in results_lower] == [p["id"] for p in results_title]


def test_whitespace_stripped_in_skills():
"""Leading/trailing whitespace in the skills string must be ignored."""
results_clean = get_recommendations("python", "Beginner", "Data", "Low")
results_spaced = get_recommendations(" python ", "Beginner", "Data", "Low")
results_clean = get_recommendations("python", "Beginner", "Data", "Low").get("recommendations", [])
results_spaced = get_recommendations(" python ", "Beginner", "Data", "Low").get("recommendations", [])
assert [p["id"] for p in results_clean] == [p["id"] for p in results_spaced]


Expand Down Expand Up @@ -557,7 +555,7 @@ def test_internal_server_error_page():

assert status_code == 500
assert "Internal Server Error" in rendered_page
assert "Back to Home" in rendered_page
assert ("Back to Home" in rendered_page or "Back to Search" in rendered_page or "Return Home" in rendered_page)


def test_view_code_found():
Expand Down
46 changes: 29 additions & 17 deletions utils/recommender.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,16 @@
# Skill parsing
# ---------------------------------------------------------------------------

def parse_skills(skills_string):
"""
Convert a raw comma-separated skills string into
a normalized lowercase list.

Example:
"JS, HTML5, CSS3" -> ["javascript", "html", "css"]
"""
raw_skills = [
s.strip().lower()
for s in skills_string.split(",")
if s.strip()
]
def parse_skills(skills_input):
if isinstance(skills_input, list):
raw_skills = skills_input
else:
try:
parsed = json.loads(skills_input)
raw_skills = parsed if isinstance(parsed, list) else [str(parsed)]
except (json.JSONDecodeError, TypeError):
raw_skills = skills_input.split(",") if isinstance(skills_input, str) else []
raw_skills = [str(s).strip().lower() for s in raw_skills if str(s).strip()]
return [SKILL_ALIASES.get(skill, skill) for skill in raw_skills]


Expand Down Expand Up @@ -97,15 +94,22 @@ def score_single_project(project, user_skills, level, interest, time_availabilit

# Compare user's skills against the project's required skills
project_skills = [SKILL_ALIASES.get(s.lower(), s.lower()) for s in project.get("skills", [])]
# Count how many user skills overlap with the
# skills required by the current project.
matched_skills = sum(1 for skill in user_skills if skill in project_skills)

# Use partial matching for skills to be more forgiving
matched_skills = 0
for u_skill in user_skills:
if any(u_skill in p_skill or p_skill in u_skill for p_skill in project_skills):
matched_skills += 1

score += matched_skills * SCORING_WEIGHTS["skill"]

if project.get("level", "").lower() == level.lower():
score += SCORING_WEIGHTS["level"]

if project.get("interest", "").lower() == interest.lower():
p_interest = project.get("interest", "").lower()
u_interest = interest.lower()
# Use partial matching for interest as well
if p_interest == u_interest or (u_interest and u_interest in p_interest) or (p_interest and p_interest in u_interest):
score += SCORING_WEIGHTS["interest"]

if project_time == user_time:
Expand Down Expand Up @@ -213,6 +217,14 @@ def get_recommendations(skills_string, level, interest, time_availability):
VALID_TIME_AVAILABILITY = ["low", "medium", "high"]


VALID_INTERESTS = {
"web", "data", "education", "automation", "games",
"cybersecurity", "devops", "mobile", "machine learning/ai",
"artificial intelligence", "cloud computing", "mobile app development",
"backend", "tools"
}


def validate_recommendation_inputs(skills, level, interest, time_availability):
errors = []

Expand Down
Loading