diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 80e23874..cdba1353 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,11 +24,12 @@ jobs: - name: Install dependencies run: | pip install -r requirements-dev.txt + pip install pytest # Run tests - name: Run tests run: | - python -m unittest tests/test_basic.py + PYTHONPATH=src pytest # Optional: Print success - name: Success message diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index da21d874..8647362c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -29,7 +29,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install flake8 + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + pip install flake8 pytest - name: Lint code (non-blocking) run: | @@ -37,4 +38,4 @@ jobs: - name: Run tests run: | - python -m unittest tests/test_basic.py + PYTHONPATH=src pytest diff --git a/data/devpath.db b/data/devpath.db new file mode 100644 index 00000000..47760a9c Binary files /dev/null and b/data/devpath.db differ diff --git a/rewrite_explore.py b/rewrite_explore.py new file mode 100644 index 00000000..4a03aa86 --- /dev/null +++ b/rewrite_explore.py @@ -0,0 +1,156 @@ +import re + +with open("src/templates/explore.html", "r") as f: + content = f.read() + +# Replace the hero section, stats section, etc. with a simple explore layout. +# We will find the
tag or
and remove everything until
+""" + new_body = new_body.replace("{% end从业%}", "{% endfor %}") + content = content[:start_idx] + new_body + content[end_idx:] + +with open("src/templates/explore.html", "w") as f: + f.write(content) + +print("Done") diff --git a/src/routes/main_routes.py b/src/routes/main_routes.py index 161f0f26..9f60e274 100644 --- a/src/routes/main_routes.py +++ b/src/routes/main_routes.py @@ -28,6 +28,7 @@ from flask import jsonify from utils.portfolio_analyzer import analyze_portfolio import os +import math from models import db, ProjectProgress _skill_validator = SkillProgressionValidator() @@ -68,6 +69,86 @@ def index(): return render_template("index.html", stats=stats, available_levels=available_levels, available_interests=available_interests, config=Config) +@main.route("/explore") +def explore(): + """Render the explore page with server-side pagination, filtering, and sorting.""" + page = request.args.get("page", 1, type=int) + per_page = request.args.get("per_page", 12, type=int) + search_query = request.args.get("search", "").strip().lower() + level_filter = request.args.get("level", "").strip().lower() + interest_filter = request.args.get("interest", "").strip().lower() + time_filter = request.args.get("time", "").strip().lower() + sort_by = request.args.get("sort", "id_asc") + + all_projects = load_all_projects() + filtered_projects = [] + + for p in all_projects: + # Search text + if search_query: + searchable_text = ( + p.get("title", "") + " " + + p.get("description", "") + " " + + " ".join(p.get("skills", [])) + ).lower() + if search_query not in searchable_text: + continue + + if level_filter and p.get("level", "").lower() != level_filter: + continue + + if interest_filter and p.get("interest", "").lower() != interest_filter: + continue + + if time_filter and p.get("time", "").lower() != time_filter: + continue + + filtered_projects.append(p) + + # Sorting + if sort_by == "title_asc": + filtered_projects.sort(key=lambda x: x.get("title", "").lower()) + elif sort_by == "title_desc": + filtered_projects.sort(key=lambda x: x.get("title", "").lower(), reverse=True) + elif sort_by == "id_desc": + filtered_projects.sort(key=lambda x: x.get("id", 0), reverse=True) + else: # id_asc + filtered_projects.sort(key=lambda x: x.get("id", 0)) + + total_items = len(filtered_projects) + total_pages = math.ceil(total_items / per_page) if total_items > 0 else 1 + + # Bound page number + if page < 1: + page = 1 + elif page > total_pages: + page = total_pages + + start_idx = (page - 1) * per_page + end_idx = start_idx + per_page + + paginated_projects = filtered_projects[start_idx:end_idx] + + # Also pass filter dropdown options to UI + available_levels = get_available_levels() + stats = get_project_stats() + + return render_template( + "explore.html", + projects=paginated_projects, + page=page, + total_pages=total_pages, + total_items=total_items, + search=search_query, + level=level_filter, + interest=interest_filter, + time=time_filter, + sort=sort_by, + available_levels=available_levels, + stats=stats, + config=Config + ) + @main.route("/contact") def contact(): return render_template("contact.html", config=Config) diff --git a/src/templates/contact.html b/src/templates/contact.html index 9eb28451..ec52900f 100644 --- a/src/templates/contact.html +++ b/src/templates/contact.html @@ -117,7 +117,7 @@

-
+ @@ -161,9 +161,6 @@

- - - diff --git a/src/templates/explore.html b/src/templates/explore.html new file mode 100644 index 00000000..8568f802 --- /dev/null +++ b/src/templates/explore.html @@ -0,0 +1,737 @@ + + + + + + + + DevPath — Find Projects Based On Your Skills + + + + + + + + + + + + + + + + + + + + {% include 'partials/theme_head.html' %} + + + + + + + + + + + + + + + + +
+

Explore All Projects

+

Browse our entire catalog of projects, filter by difficulty or interest, and find your next coding challenge.

+ +
+ + + + +
+
+

Showing {{ projects|length }} of {{ total_items }} projects

+
+ + {% if projects %} +
+ {% for project in projects %} +
+

{{ project.title }}

+

{{ project.description | truncate(120) }}

+
+ {% for skill in project.skills %} + {{ skill }} + {% endfor %} + {{ project.level }} + Time: {{ project.time }} +
+ +
+ {% endfor %} +
+ + + + {% else %} +
+

No Projects Found

+

Try adjusting your filters or search term.

+ Clear Filters +
+ {% endif %} +
+
+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html index 3bdf348a..b82d08d6 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -455,6 +455,7 @@ How It Works Features Find Project + Explore All Compare Contact {% if current_user %} @@ -1268,6 +1269,7 @@

  • Home
  • How It Works
  • Features
  • +
  • Explore All
  • Find Project
  • Contact Us
  • diff --git a/test_recommender.py b/test_recommender.py index 8546263d..4df5ae77 100644 --- a/test_recommender.py +++ b/test_recommender.py @@ -15,6 +15,12 @@ _get_related, _load_clusters, ) +try: + from app import app + ctx = app.app_context() + ctx.push() +except ImportError: + pass # --------------------------------------------------------------------------- # Helpers diff --git a/tests/test_basic.py b/tests/test_basic.py index 24d3c9a0..e2f2974e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -14,6 +14,7 @@ score_single_project, SCORING_WEIGHTS, VALID_LEVELS, + VALID_INTERESTS, VALID_TIME_AVAILABILITY, ) from utils.roadmap_comparer import compare_roadmaps, load_all_career_roadmaps @@ -311,6 +312,31 @@ def test_home_route(): assert response.status_code == 200 +def test_explore_route(): + """Explore route should return 200 OK and handle pagination.""" + client = get_client() + response = client.get("/explore?page=1&per_page=5") + assert response.status_code == 200 + html = response.data.decode("utf-8") + assert "Explore All Projects" in html + # The max per_page is 5, so there should be pagination controls or fewer items. + assert 'class="project-card"' in html + +def test_contact_page_renders_send_message_form(): + """Contact page should include the external form handler and required fields.""" + client = get_client() + response = client.get("/contact") + + assert response.status_code == 200 + html = response.get_data(as_text=True) + + assert 'class="contact-form"' in html + assert 'action="https://api.web3forms.com/submit"' in html + assert 'method="POST"' in html + assert 'name="name"' in html + assert 'name="email"' in html + assert 'name="message"' in html + assert "Send Message" in html def test_security_headers_present(): """Security headers should be included in all responses.""" client = get_client()