├── requirements.txt
├── src
├── backend
│ ├── __init__.py
│ ├── routers
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ └── activities.py
│ └── database.py
├── app.py
├── README.md
├── .gitignore
└── static
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── .devcontainer
├── postCreate.sh
├── devcontainer.json
└── installMongoDB.sh
├── .vscode
└── launch.json
├── .github
├── steps
│ ├── 5-merge.md
│ ├── x-review.md
│ ├── 2-prepare-to-collaborate.md
│ ├── 3-foster-healthy-growth.md
│ ├── 4-prepare-for-the-inevitable.md
│ └── 1-protect-your-code.md
└── workflows
│ ├── 0-start-exercise.yml
│ ├── 5-merge.yml
│ ├── 1-protect-your-code.yml
│ ├── 2-prepare-to-collaborate.yml
│ ├── 4-prepare-for-the-inevitable.yml
│ └── 3-foster-healthy-growth.yml
├── LICENSE
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | pymongo
4 | argon2-cffi==23.1.0
--------------------------------------------------------------------------------
/src/backend/__init__.py:
--------------------------------------------------------------------------------
1 | from . import routers
2 | from . import database
--------------------------------------------------------------------------------
/src/backend/routers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import activities
2 | from . import auth
--------------------------------------------------------------------------------
/.devcontainer/postCreate.sh:
--------------------------------------------------------------------------------
1 | # Prepare python environment
2 | pip install -r requirements.txt
3 |
4 | # Prepare MongoDB Dev DB
5 | ./.devcontainer/installMongoDB.sh
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Python 3",
3 | "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13",
4 | "forwardPorts": [8000],
5 | "postCreateCommand": "bash ./.devcontainer/postCreate.sh",
6 | "customizations": {
7 | "vscode": {
8 | "extensions": [
9 | "GitHub.copilot",
10 | "ms-python.python",
11 | "ms-python.debugpy",
12 | "mongodb.mongodb-vscode"
13 | ]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch Mergington WebApp",
9 | "type": "debugpy",
10 | "request": "launch",
11 | "module": "uvicorn",
12 | "args": [
13 | "src.app:app",
14 | "--reload"
15 | ],
16 | "jinja": true
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.github/steps/5-merge.md:
--------------------------------------------------------------------------------
1 | # Step 5: Release
2 |
3 | With all our preparations ready, it's time to release them!
4 |
5 | ## ⌨️ Activity: Merge our collaboration changes
6 |
7 | 1. In the top navigation, select the **Pull requests** tab.
8 |
9 | 1. Find the pull request for the `prepare-to-collaborate` branch and merge it. You may need to wait for your new security scans to finish.
10 |
11 | 1. Find the pull request for the `add-issue-templates` branch and merge it. You may need to wait for your new security scans to finish.
12 |
13 | 1. With both pull requests merged, Mona will prepare the final review and acknowledge the exercise as finished! Nice work! You are all done! 🎉
14 |
--------------------------------------------------------------------------------
/.devcontainer/installMongoDB.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install MongoDB
4 | curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/mongodb-server-7.0.gpg
5 | echo "deb [ arch=amd64,arm64 signed-by=/etc/apt/trusted.gpg.d/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
6 | sudo apt-get update
7 | sudo apt-get install -y mongodb-org
8 |
9 | # Create necessary directories and set permissions
10 | sudo mkdir -p /data/db
11 | sudo chown -R mongodb:mongodb /data/db
12 |
13 | # Start MongoDB service
14 | sudo mongod --fork --logpath /var/log/mongodb/mongod.log
15 |
16 | echo "MongoDB has been installed and started successfully!"
17 | mongod --version
18 |
19 | # Run sample MongoDB commands
20 | echo "Current databases:"
21 | mongosh --eval "db.getMongo().getDBNames()"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 GitHub Skills
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/app.py:
--------------------------------------------------------------------------------
1 | """
2 | High School Management System API
3 |
4 | A super simple FastAPI application that allows students to view and sign up
5 | for extracurricular activities at Mergington High School.
6 | """
7 |
8 | from fastapi import FastAPI
9 | from fastapi.staticfiles import StaticFiles
10 | from fastapi.responses import RedirectResponse
11 | import os
12 | from pathlib import Path
13 | from .backend import routers, database
14 |
15 | # Initialize web host
16 | app = FastAPI(
17 | title="Mergington High School API",
18 | description="API for viewing and signing up for extracurricular activities"
19 | )
20 |
21 | # Initialize database with sample data if empty
22 | database.init_database()
23 |
24 | # Mount the static files directory for serving the frontend
25 | current_dir = Path(__file__).parent
26 | app.mount("/static", StaticFiles(directory=os.path.join(current_dir, "static")), name="static")
27 |
28 | # Root endpoint to redirect to static index.html
29 | @app.get("/")
30 | def root():
31 | return RedirectResponse(url="/static/index.html")
32 |
33 | # Include routers
34 | app.include_router(routers.activities.router)
35 | app.include_router(routers.auth.router)
36 |
--------------------------------------------------------------------------------
/src/backend/routers/auth.py:
--------------------------------------------------------------------------------
1 | """
2 | Authentication endpoints for the High School Management System API
3 | """
4 |
5 | from fastapi import APIRouter, HTTPException
6 | from typing import Dict, Any
7 |
8 | from ..database import teachers_collection, verify_password
9 |
10 | router = APIRouter(
11 | prefix="/auth",
12 | tags=["auth"]
13 | )
14 |
15 | # Password hashing/verification handled by Argon2 functions in database.py
16 |
17 | @router.post("/login")
18 | def login(username: str, password: str) -> Dict[str, Any]:
19 | """Login a teacher account"""
20 | # Find the teacher in the database
21 | teacher = teachers_collection.find_one({"_id": username})
22 |
23 | # Verify password using Argon2 verifier from database.py
24 | if not teacher or not verify_password(teacher.get("password", ""), password):
25 | raise HTTPException(status_code=401, detail="Invalid username or password")
26 |
27 | # Return teacher information (excluding password)
28 | return {
29 | "username": teacher["username"],
30 | "display_name": teacher["display_name"],
31 | "role": teacher["role"]
32 | }
33 |
34 | @router.get("/check-session")
35 | def check_session(username: str) -> Dict[str, Any]:
36 | """Check if a session is valid by username"""
37 | teacher = teachers_collection.find_one({"_id": username})
38 |
39 | if not teacher:
40 | raise HTTPException(status_code=404, detail="Teacher not found")
41 |
42 | return {
43 | "username": teacher["username"],
44 | "display_name": teacher["display_name"],
45 | "role": teacher["role"]
46 | }
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | # Mergington High School Activities API
2 |
3 | A super simple FastAPI application that allows students to view and sign up for extracurricular activities.
4 |
5 | ## Features
6 |
7 | - View all available extracurricular activities
8 | - Sign up for activities
9 |
10 | ## Getting Started
11 |
12 | 1. Install the dependencies:
13 |
14 | ```
15 | pip install fastapi uvicorn
16 | ```
17 |
18 | 2. Run the application:
19 |
20 | ```
21 | python app.py
22 | ```
23 |
24 | 3. Open your browser and go to:
25 | - API documentation: http://localhost:8000/docs
26 | - Alternative documentation: http://localhost:8000/redoc
27 |
28 | ## API Endpoints
29 |
30 | | Method | Endpoint | Description |
31 | | ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
32 | | GET | `/activities` | Get all activities with their details and current participant count |
33 | | POST | `/activities/{activity_name}/signup?email=student@mergington.edu` | Sign up for an activity |
34 |
35 | ## Data Model
36 |
37 | The application uses a simple data model with meaningful identifiers:
38 |
39 | 1. **Activities** - Uses activity name as identifier:
40 |
41 | - Description
42 | - Schedule
43 | - Maximum number of participants allowed
44 | - List of student emails who are signed up
45 |
46 | 2. **Students** - Uses email as identifier:
47 | - Name
48 | - Grade level
49 |
50 | All data is stored in memory, which means data will be reset when the server restarts.
51 |
--------------------------------------------------------------------------------
/.github/workflows/0-start-exercise.yml:
--------------------------------------------------------------------------------
1 | name: Step 0 # Start Exercise
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | actions: write
11 | issues: write
12 |
13 | env:
14 | STEP_1_FILE: ".github/steps/1-protect-your-code.md"
15 |
16 | jobs:
17 | start_exercise:
18 | if: |
19 | !github.event.repository.is_template
20 | name: Start Exercise
21 | uses: skills/exercise-toolkit/.github/workflows/start-exercise.yml@v0.6.0
22 | with:
23 | exercise-title: "Introduction to Repository Management"
24 | intro-message: "In this exercise, you'll configure your repository for easier collaboration. You'll learn how to protect your code, prepare for collaboration, and foster healthy growth in your projects."
25 |
26 | post_next_step_content:
27 | name: Post next step content
28 | runs-on: ubuntu-latest
29 | needs: [start_exercise]
30 | env:
31 | ISSUE_URL: ${{ needs.start_exercise.outputs.issue-url }}
32 |
33 | steps:
34 |
35 | - name: Checkout
36 | uses: actions/checkout@v4
37 |
38 | - name: Load Exercise Toolkit
39 | uses: actions/checkout@v4
40 | with:
41 | repository: skills/exercise-toolkit
42 | path: exercise-toolkit
43 | ref: v0.6.0
44 |
45 | - name: Configure Git user
46 | run: |
47 | git config user.name github-actions[bot]
48 | git config user.email github-actions[bot]@users.noreply.github.com
49 |
50 | - name: Build comment - add step content
51 | id: build-comment
52 | uses: skills/action-text-variables@v2
53 | with:
54 | template-file: ${{ env.STEP_1_FILE }}
55 | template-vars: |
56 | login: ${{ github.actor }}
57 | full_repo_name: ${{ github.repository }}
58 |
59 | - name: Create comment - add step content
60 | run: |
61 | gh issue comment "$ISSUE_URL" \
62 | --body "$ISSUE_BODY"
63 | env:
64 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65 | ISSUE_BODY: ${{ steps.build-comment.outputs.updated-text }}
66 |
67 | - name: Create comment - watching for progress
68 | run: |
69 | gh issue comment "$ISSUE_URL" \
70 | --body-file "exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md"
71 | env:
72 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73 |
74 | - name: Disable current workflow and enable next one
75 | run: |
76 | # gh workflow enable "Step 0" # Already disabled
77 | gh workflow enable "Step 1" || true
78 | env:
79 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80 |
--------------------------------------------------------------------------------
/.github/steps/x-review.md:
--------------------------------------------------------------------------------
1 | ## Review
2 |
3 | _Congratulations, you've completed this exercise! You're all set for an awesome time collaborating with your fellow teachers!_
4 |
5 |
6 |
7 | You've successfully prepared Mergington High's extracurricular activities website for healthy and safe collaboration. Make sure you let the principal know so he can brag to the IT department about your proactive efforts!
8 |
9 | Here is a snippet of something you can share:
10 |
11 | - Protected our code from accidental mistakes with `.gitignore` and branch protections.
12 | - Set clear guidelines for teacher contributions with `CONTRIBUTING.md` and `CODEOWNERS`.
13 | - Established community standards with a Code of Conduct and structured issue templates.
14 | - Prepared for the future security challenges by enabling automated scanning and providing safe submission procedures.
15 |
16 | ### What's next?
17 |
18 | This exercise was meant to introduce you to many of the different areas of managing a repository. However, there is still more to learn!
19 |
20 | Here are some additional exercises for a deeper dive:
21 |
22 | - [Skills: Secure your repository supply chain](https://github.com/skills/secure-repository-supply-chain)
23 | - [Skills: Introduction to CodeQL](https://github.com/skills/introduction-to-codeql)
24 | - [Skills: Introduction to Secret Scanning](https://github.com/skills/introduction-to-secret-scanning)
25 |
26 | Here are some useful references from the [GitHub Docs](https://docs.github.com/en):
27 |
28 | - [How to ignore files](https://docs.github.com/en/get-started/git-basics/ignoring-files)
29 | - [Template gitignore files](https://github.com/github/gitignore)
30 | - [Creating rulesets for a repository](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository#using-fnmatch-syntax)
31 | - [Managing a branch protection rule](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule)
32 | - [About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners)
33 | - [Setting guidelines for contributors](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors)
34 | - [Add a code of conduct](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project)
35 | - [Configuring default setup for code scanning](https://docs.github.com/en/code-security/code-scanning/enabling-code-scanning/configuring-default-setup-for-code-scanning)
36 | - [Adding a security policy](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository)
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Introduction to Repository Management
2 |
3 | _Learn the basics of several GitHub features that can help support a collaborative, friendly, and healthy project._
4 |
5 | ## Welcome
6 |
7 | - **Who is this for**: Developers with the need to start collaborating.
8 | - **What you'll learn**: The different ways to protect your repository's content as more people join as collaborators.
9 | - **What you'll build**: You will prepare Mergington High School's extracurricular activities website repository so additional teachers can safely collaborate.
10 | - **Prerequisites**:
11 | - Skills exercise: [Introduction to GitHub](https://github.com/skills/introduction-to-github)
12 | - Skills exercise: [Communicate using Markdown](https://github.com/skills/communicate-using-markdown)
13 | - Skills exercise: [Review pull requests](https://github.com/skills/review-pull-requests)
14 | - **How long**: This exercise takes less than one hour to complete.
15 |
16 | In this exercise, you will:
17 |
18 | 1. Add a simple rulesets and configuration to restrict repository content.
19 | 1. Communicate procedures to help guide collaborators.
20 | 1. Assign responsibility of parts of the code to particular collaborators.
21 | 1. Learn the difference between collaboration in a personal repository and organization repository.
22 | 1. Establish ground rules to promote a health collaboration environment.
23 | 1. Establish a process for managing security updates.
24 |
25 | > [!IMPORTANT]
26 | > This exercise is meant to provide an overview of many GitHub features.
27 | > It will provide references to learn more but not a detailed explanation for any specific subject.
28 |
29 | ### How to start this exercise
30 |
31 | Simply copy the exercise to your account, then give your favorite Octocat (Mona) **about 20 seconds** to prepare the first lesson, then **refresh the page**.
32 |
33 | [](https://github.com/new?template_owner=skills&template_name=introduction-to-repository-management&owner=%40me&name=skills-introduction-to-repository-management&description=Exercise:+introduction+to+repository+management&visibility=public)
34 |
35 |
36 | Having trouble? 🤷
37 |
38 | When copying the exercise, we recommend the following settings:
39 |
40 | - For owner, choose your personal account or an organization to host the repository.
41 |
42 | - We recommend creating a public repository, since private repositories will use Actions minutes.
43 |
44 | If the exercise isn't ready in 20 seconds, please check the [Actions](../../actions) tab.
45 |
46 | - Check to see if a job is running. Sometimes it simply takes a bit longer.
47 |
48 | - If the page shows a failed job, please submit an issue. Nice, you found a bug! 🐛
49 |
50 |
51 |
52 | ---
53 |
54 | © 2025 GitHub • [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md) • [MIT License](https://gh.io/mit)
55 |
--------------------------------------------------------------------------------
/.github/workflows/5-merge.yml:
--------------------------------------------------------------------------------
1 | name: Step 5
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | types:
8 | - closed
9 |
10 | concurrency:
11 | group: ${{ github.workflow }}
12 | cancel-in-progress: true
13 |
14 | permissions:
15 | contents: write
16 | actions: write
17 | issues: write
18 |
19 | env:
20 | REVIEW_FILE: ".github/steps/x-review.md"
21 |
22 | jobs:
23 | find_exercise:
24 | if: |
25 | !github.event.repository.is_template
26 | name: Find Exercise Issue
27 | uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.6.0
28 |
29 | check_step_work:
30 | name: Check step work
31 | if: github.event.pull_request.merged == true
32 | runs-on: ubuntu-latest
33 | needs: [find_exercise]
34 | env:
35 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
36 |
37 | steps:
38 | - name: Checkout
39 | uses: actions/checkout@v4
40 |
41 | - name: Get Exercise Toolkit
42 | uses: actions/checkout@v4
43 | with:
44 | repository: skills/exercise-toolkit
45 | path: exercise-toolkit
46 | ref: v0.6.0
47 |
48 | - name: Update comment - checking work
49 | run: |
50 | gh issue comment "$ISSUE_URL" \
51 | --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
52 | --edit-last
53 | env:
54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 |
56 | # START: Check practical exercise
57 |
58 | # Merging the pull request is enough to finish this step
59 |
60 | # END: Check practical exercise
61 |
62 | - name: Update comment - step finished - final review next
63 | run: |
64 | gh issue comment "$ISSUE_URL" \
65 | --body-file exercise-toolkit/markdown-templates/step-feedback/lesson-review.md \
66 | --edit-last
67 | env:
68 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69 |
70 | post_review_content:
71 | name: Post review content
72 | runs-on: ubuntu-latest
73 | needs: [find_exercise, check_step_work]
74 | env:
75 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
76 |
77 | steps:
78 | - name: Checkout
79 | uses: actions/checkout@v4
80 |
81 | - name: Create comment - add review content
82 | run: |
83 | gh issue comment "$ISSUE_URL" \
84 | --body-file ${{ env.REVIEW_FILE }}
85 | env:
86 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87 |
88 | finish_exercise:
89 | name: Finish Exercise
90 | needs: [find_exercise, post_review_content]
91 | uses: skills/exercise-toolkit/.github/workflows/finish-exercise.yml@v0.6.0
92 | with:
93 | issue-url: ${{ needs.find_exercise.outputs.issue-url }}
94 | exercise-title: "Introduction to Repository Management"
95 | update-readme-with-congratulations: false
96 |
97 | disable_workflow:
98 | name: Disable this workflow
99 | needs: [find_exercise, post_review_content]
100 | runs-on: ubuntu-latest
101 |
102 | steps:
103 | - name: Checkout
104 | uses: actions/checkout@v4
105 | - name: Disable current workflow
106 | run: gh workflow disable "${{github.workflow}}" || true
107 | env:
108 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109 |
--------------------------------------------------------------------------------
/.github/workflows/1-protect-your-code.yml:
--------------------------------------------------------------------------------
1 | name: Step 1
2 |
3 | on:
4 | push:
5 | branches:
6 | - prepare-to-collaborate
7 | paths:
8 | - ".gitignore"
9 |
10 | permissions:
11 | contents: write
12 | actions: write
13 | issues: write
14 |
15 | env:
16 | STEP_2_FILE: ".github/steps/2-prepare-to-collaborate.md"
17 |
18 | jobs:
19 | find_exercise:
20 | if: |
21 | !github.event.repository.is_template
22 | name: Find Exercise Issue
23 | uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.6.0
24 |
25 | check_step_work:
26 | name: Check step work
27 | runs-on: ubuntu-latest
28 | needs: [find_exercise]
29 | env:
30 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
31 |
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v4
35 |
36 | - name: Load Exercise Toolkit
37 | uses: actions/checkout@v4
38 | with:
39 | repository: skills/exercise-toolkit
40 | path: exercise-toolkit
41 | ref: v0.6.0
42 |
43 | # START: Check practical exercise
44 |
45 | # Creating the new .gitignore file is enough to finish this step.
46 |
47 | # END: Check practical exercise
48 |
49 | - name: Build message - step finished
50 | id: build-message-step-finish
51 | uses: skills/action-text-variables@v2
52 | with:
53 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
54 | template-vars: |
55 | next_step_number: 2
56 |
57 | - name: Update comment - step finished
58 | run: |
59 | gh issue comment "$ISSUE_URL" \
60 | --body "$ISSUE_BODY" \
61 | --edit-last
62 | env:
63 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64 | ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
65 |
66 | post_next_step_content:
67 | name: Post next step content
68 | runs-on: ubuntu-latest
69 | needs: [find_exercise, check_step_work]
70 | env:
71 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
72 |
73 | steps:
74 | - name: Checkout
75 | uses: actions/checkout@v4
76 |
77 | - name: Get Exercise Toolkit
78 | uses: actions/checkout@v4
79 | with:
80 | repository: skills/exercise-toolkit
81 | path: exercise-toolkit
82 | ref: v0.6.0
83 |
84 | - name: Build message - add step content
85 | id: build-message-add-step-content
86 | uses: skills/action-text-variables@v2
87 | with:
88 | template-file: ${{ env.STEP_2_FILE }}
89 | template-vars: |
90 | login: ${{ github.actor }}
91 |
92 | - name: Create comment - step results
93 | run: |
94 | gh issue comment "$ISSUE_URL" \
95 | --body "$COMMENT_BODY"
96 | env:
97 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
98 | COMMENT_BODY: ${{ steps.build-message-add-step-content.outputs.updated-text }}
99 |
100 | - name: Create comment - watching for progress
101 | run: |
102 | gh issue comment "$ISSUE_URL" \
103 | --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
104 | env:
105 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
106 |
107 | - name: Disable current workflow and enable next one
108 | run: |
109 | gh workflow disable "Step 1" || true
110 | gh workflow enable "Step 2" || true
111 | env:
112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113 |
--------------------------------------------------------------------------------
/src/backend/routers/activities.py:
--------------------------------------------------------------------------------
1 | """
2 | Endpoints for the High School Management System API
3 | """
4 |
5 | from fastapi import APIRouter, HTTPException, Query
6 | from fastapi.responses import RedirectResponse
7 | from typing import Dict, Any, Optional, List
8 |
9 | from ..database import activities_collection, teachers_collection
10 |
11 | router = APIRouter(
12 | prefix="/activities",
13 | tags=["activities"]
14 | )
15 |
16 | @router.get("", response_model=Dict[str, Any])
17 | @router.get("/", response_model=Dict[str, Any])
18 | def get_activities(
19 | day: Optional[str] = None,
20 | start_time: Optional[str] = None,
21 | end_time: Optional[str] = None
22 | ) -> Dict[str, Any]:
23 | """
24 | Get all activities with their details, with optional filtering by day and time
25 |
26 | - day: Filter activities occurring on this day (e.g., 'Monday', 'Tuesday')
27 | - start_time: Filter activities starting at or after this time (24-hour format, e.g., '14:30')
28 | - end_time: Filter activities ending at or before this time (24-hour format, e.g., '17:00')
29 | """
30 | # Build the query based on provided filters
31 | query = {}
32 |
33 | if day:
34 | query["schedule_details.days"] = {"$in": [day]}
35 |
36 | if start_time:
37 | query["schedule_details.start_time"] = {"$gte": start_time}
38 |
39 | if end_time:
40 | query["schedule_details.end_time"] = {"$lte": end_time}
41 |
42 | # Query the database
43 | activities = {}
44 | for activity in activities_collection.find(query):
45 | name = activity.pop('_id')
46 | activities[name] = activity
47 |
48 | return activities
49 |
50 | @router.get("/days", response_model=List[str])
51 | def get_available_days() -> List[str]:
52 | """Get a list of all days that have activities scheduled"""
53 | # Aggregate to get unique days across all activities
54 | pipeline = [
55 | {"$unwind": "$schedule_details.days"},
56 | {"$group": {"_id": "$schedule_details.days"}},
57 | {"$sort": {"_id": 1}} # Sort days alphabetically
58 | ]
59 |
60 | days = []
61 | for day_doc in activities_collection.aggregate(pipeline):
62 | days.append(day_doc["_id"])
63 |
64 | return days
65 |
66 | @router.post("/{activity_name}/signup")
67 | def signup_for_activity(activity_name: str, email: str, teacher_username: Optional[str] = Query(None)):
68 | """Sign up a student for an activity - requires teacher authentication"""
69 | # Check teacher authentication
70 | if not teacher_username:
71 | raise HTTPException(status_code=401, detail="Authentication required for this action")
72 |
73 | teacher = teachers_collection.find_one({"_id": teacher_username})
74 | if not teacher:
75 | raise HTTPException(status_code=401, detail="Invalid teacher credentials")
76 |
77 | # Get the activity
78 | activity = activities_collection.find_one({"_id": activity_name})
79 | if not activity:
80 | raise HTTPException(status_code=404, detail="Activity not found")
81 |
82 | # Validate student is not already signed up
83 | if email in activity["participants"]:
84 | raise HTTPException(
85 | status_code=400, detail="Already signed up for this activity")
86 |
87 | # Add student to participants
88 | result = activities_collection.update_one(
89 | {"_id": activity_name},
90 | {"$push": {"participants": email}}
91 | )
92 |
93 | if result.modified_count == 0:
94 | raise HTTPException(status_code=500, detail="Failed to update activity")
95 |
96 | return {"message": f"Signed up {email} for {activity_name}"}
97 |
98 | @router.post("/{activity_name}/unregister")
99 | def unregister_from_activity(activity_name: str, email: str, teacher_username: Optional[str] = Query(None)):
100 | """Remove a student from an activity - requires teacher authentication"""
101 | # Check teacher authentication
102 | if not teacher_username:
103 | raise HTTPException(status_code=401, detail="Authentication required for this action")
104 |
105 | teacher = teachers_collection.find_one({"_id": teacher_username})
106 | if not teacher:
107 | raise HTTPException(status_code=401, detail="Invalid teacher credentials")
108 |
109 | # Get the activity
110 | activity = activities_collection.find_one({"_id": activity_name})
111 | if not activity:
112 | raise HTTPException(status_code=404, detail="Activity not found")
113 |
114 | # Validate student is signed up
115 | if email not in activity["participants"]:
116 | raise HTTPException(
117 | status_code=400, detail="Not registered for this activity")
118 |
119 | # Remove student from participants
120 | result = activities_collection.update_one(
121 | {"_id": activity_name},
122 | {"$pull": {"participants": email}}
123 | )
124 |
125 | if result.modified_count == 0:
126 | raise HTTPException(status_code=500, detail="Failed to update activity")
127 |
128 | return {"message": f"Unregistered {email} from {activity_name}"}
--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------
1 | ####
2 | #### Python - https://github.com/github/gitignore/blob/main/Python.gitignore
3 | ####
4 |
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[codz]
8 | *$py.class
9 |
10 | # C extensions
11 | *.so
12 |
13 | # Distribution / packaging
14 | .Python
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | share/python-wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | MANIFEST
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .nox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py.cover
54 | .hypothesis/
55 | .pytest_cache/
56 | cover/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 | db.sqlite3
66 | db.sqlite3-journal
67 |
68 | # Flask stuff:
69 | instance/
70 | .webassets-cache
71 |
72 | # Scrapy stuff:
73 | .scrapy
74 |
75 | # Sphinx documentation
76 | docs/_build/
77 |
78 | # PyBuilder
79 | .pybuilder/
80 | target/
81 |
82 | # Jupyter Notebook
83 | .ipynb_checkpoints
84 |
85 | # IPython
86 | profile_default/
87 | ipython_config.py
88 |
89 | # pyenv
90 | # For a library or package, you might want to ignore these files since the code is
91 | # intended to run in multiple environments; otherwise, check them in:
92 | # .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # UV
102 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
103 | # This is especially recommended for binary packages to ensure reproducibility, and is more
104 | # commonly ignored for libraries.
105 | #uv.lock
106 |
107 | # poetry
108 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
109 | # This is especially recommended for binary packages to ensure reproducibility, and is more
110 | # commonly ignored for libraries.
111 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
112 | #poetry.lock
113 | #poetry.toml
114 |
115 | # pdm
116 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
118 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
119 | #pdm.lock
120 | #pdm.toml
121 | .pdm-python
122 | .pdm-build/
123 |
124 | # pixi
125 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
126 | #pixi.lock
127 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
128 | # in the .venv directory. It is recommended not to include this directory in version control.
129 | .pixi
130 |
131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
132 | __pypackages__/
133 |
134 | # Celery stuff
135 | celerybeat-schedule
136 | celerybeat.pid
137 |
138 | # Redis
139 | *.rdb
140 | *.aof
141 | *.pid
142 |
143 | # RabbitMQ
144 | mnesia/
145 | rabbitmq/
146 | rabbitmq-data/
147 |
148 | # ActiveMQ
149 | activemq-data/
150 |
151 | # SageMath parsed files
152 | *.sage.py
153 |
154 | # Environments
155 | .env
156 | .envrc
157 | .venv
158 | env/
159 | venv/
160 | ENV/
161 | env.bak/
162 | venv.bak/
163 |
164 | # Spyder project settings
165 | .spyderproject
166 | .spyproject
167 |
168 | # Rope project settings
169 | .ropeproject
170 |
171 | # mkdocs documentation
172 | /site
173 |
174 | # mypy
175 | .mypy_cache/
176 | .dmypy.json
177 | dmypy.json
178 |
179 | # Pyre type checker
180 | .pyre/
181 |
182 | # pytype static type analyzer
183 | .pytype/
184 |
185 | # Cython debug symbols
186 | cython_debug/
187 |
188 | # PyCharm
189 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
190 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
191 | # and can be added to the global gitignore or merged into this file. For a more nuclear
192 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
193 | #.idea/
194 |
195 | # Abstra
196 | # Abstra is an AI-powered process automation framework.
197 | # Ignore directories containing user credentials, local state, and settings.
198 | # Learn more at https://abstra.io/docs
199 | .abstra/
200 |
201 | # Visual Studio Code
202 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
203 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
204 | # and can be added to the global gitignore or merged into this file. However, if you prefer,
205 | # you could uncomment the following to ignore the entire vscode folder
206 | # .vscode/
207 |
208 | # Ruff stuff:
209 | .ruff_cache/
210 |
211 | # PyPI configuration file
212 | .pypirc
213 |
214 | # Marimo
215 | marimo/_static/
216 | marimo/_lsp/
217 | __marimo__/
218 |
219 | # Streamlit
220 | .streamlit/secrets.toml
--------------------------------------------------------------------------------
/.github/workflows/2-prepare-to-collaborate.yml:
--------------------------------------------------------------------------------
1 | name: Step 2
2 |
3 | on:
4 | push:
5 | branches:
6 | - prepare-to-collaborate
7 | paths:
8 | - "CONTRIBUTING.md"
9 | - "CODEOWNERS"
10 |
11 | permissions:
12 | contents: read
13 | actions: write
14 | issues: write
15 |
16 | env:
17 | STEP_3_FILE: ".github/steps/3-foster-healthy-growth.md"
18 |
19 | jobs:
20 | find_exercise:
21 | if: |
22 | !github.event.repository.is_template
23 | name: Find Exercise Issue
24 | uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.6.0
25 |
26 | check_step_work:
27 | name: Check step work
28 | runs-on: ubuntu-latest
29 | needs: [find_exercise]
30 | env:
31 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
32 |
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 |
37 | - name: Get Exercise Toolkit
38 | uses: actions/checkout@v4
39 | with:
40 | repository: skills/exercise-toolkit
41 | path: exercise-toolkit
42 | # Results table still uses old format. Needs refactored to update.
43 | ref: v0.3.0
44 |
45 | - name: Update comment - checking work
46 | run: |
47 | gh issue comment "$ISSUE_URL" \
48 | --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
49 | --edit-last
50 | env:
51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 |
53 | # START: Check practical exercise
54 |
55 | - name: Check CONTRIBUTING.md exists
56 | id: check-contributing
57 | uses: actions/github-script@v7
58 | with:
59 | script: |
60 | const fs = require('fs');
61 |
62 | // Result object to store the message
63 | let result = {
64 | name: 'CONTRIBUTING.md',
65 | passed: true,
66 | message: ''
67 | }
68 |
69 | // Check that file exists
70 | if (!fs.existsSync('CONTRIBUTING.md')) {
71 | result.passed = false;
72 | result.message = 'File is missing.';
73 | }
74 |
75 | return result;
76 |
77 | - name: Check CODEOWNERS exists
78 | id: check-codeowners
79 | uses: actions/github-script@v7
80 | with:
81 | script: |
82 | const fs = require('fs');
83 |
84 | // Result object to store the message
85 | let result = {
86 | name: 'CODEOWNERS',
87 | passed: true,
88 | message: ''
89 | }
90 |
91 | // Check that file exists
92 | if (!fs.existsSync('CODEOWNERS')) {
93 | result.passed = false;
94 | result.message = 'File is missing.';
95 | }
96 |
97 | return result;
98 |
99 | - name: Check all results
100 | id: check-all-results
101 | uses: actions/github-script@v7
102 | with:
103 | script: |
104 | const checks = [
105 | JSON.parse(process.env['check1']),
106 | JSON.parse(process.env['check2']),
107 | ];
108 |
109 | const result = checks.every(check => check.passed);
110 | return result
111 | env:
112 | check1: ${{ steps.check-contributing.outputs.result }}
113 | check2: ${{ steps.check-codeowners.outputs.result }}
114 |
115 | - name: Build message - step results
116 | id: build-message-step-results
117 | uses: skills/action-text-variables@v2
118 | with:
119 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md
120 | template-vars: >
121 | {
122 | "step_number": 2,
123 | "passed": ${{ steps.check-all-results.outputs.result }},
124 | "results_table": [
125 | ${{ steps.check-contributing.outputs.result }},
126 | ${{ steps.check-codeowners.outputs.result }}
127 | ]
128 | }
129 |
130 | - name: Create comment - step results
131 | run: |
132 | gh issue comment "$ISSUE_URL" \
133 | --body "$COMMENT_BODY" \
134 | --edit-last
135 | env:
136 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
137 | COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }}
138 |
139 | - name: Fail job if not all checks passed
140 | if: steps.check-all-results.outputs.result == 'false'
141 | run: exit 1
142 |
143 | # END: Check practical exercise
144 |
145 | - name: Build message - step finished
146 | id: build-message-step-finish
147 | uses: skills/action-text-variables@v2
148 | with:
149 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
150 | template-vars: |
151 | next_step_number: 3
152 |
153 | - name: Update comment - step finished
154 | run: |
155 | gh issue comment "$ISSUE_URL" \
156 | --body "$ISSUE_BODY"
157 | env:
158 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159 | ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
160 |
161 | post_next_step_content:
162 | name: Post next step content
163 | runs-on: ubuntu-latest
164 | needs: [find_exercise, check_step_work]
165 | env:
166 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
167 |
168 | steps:
169 | - name: Checkout
170 | uses: actions/checkout@v4
171 |
172 | - name: Get Exercise Toolkit
173 | uses: actions/checkout@v4
174 | with:
175 | repository: skills/exercise-toolkit
176 | path: exercise-toolkit
177 | ref: v0.6.0
178 |
179 | - name: Create comment - add step content
180 | run: |
181 | gh issue comment "$ISSUE_URL" \
182 | --body-file "$STEP_3_FILE"
183 | env:
184 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
185 |
186 | - name: Create comment - watching for progress
187 | run: |
188 | gh issue comment "$ISSUE_URL" \
189 | --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
190 | env:
191 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
192 |
193 | - name: Disable current workflow and enable next one
194 | run: |
195 | gh workflow disable "Step 2" || true
196 | gh workflow enable "Step 3" || true
197 | env:
198 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
199 |
--------------------------------------------------------------------------------
/.github/workflows/4-prepare-for-the-inevitable.yml:
--------------------------------------------------------------------------------
1 | name: Step 4
2 |
3 | on:
4 | push:
5 | branches:
6 | - prepare-to-collaborate
7 | paths:
8 | - ".github/dependabot.yml"
9 | - "SECURITY.md"
10 |
11 | permissions:
12 | contents: write
13 | actions: write
14 | issues: write
15 |
16 | env:
17 | STEP_5_FILE: ".github/steps/5-merge.md"
18 |
19 | jobs:
20 | find_exercise:
21 | if: |
22 | !github.event.repository.is_template
23 | name: Find Exercise Issue
24 | uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.6.0
25 |
26 | check_step_work:
27 | name: Check step work
28 | runs-on: ubuntu-latest
29 | needs: [find_exercise]
30 | env:
31 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
32 |
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 |
37 | - name: Get Exercise Toolkit
38 | uses: actions/checkout@v4
39 | with:
40 | repository: skills/exercise-toolkit
41 | path: exercise-toolkit
42 | # Results table still uses old format. Needs refactored to update.
43 | ref: v0.3.0
44 |
45 | - name: Update comment - checking work
46 | run: |
47 | gh issue comment "$ISSUE_URL" \
48 | --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
49 | --edit-last
50 | env:
51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 |
53 | # START: Check practical exercise
54 |
55 | - name: Check for Dependabot config
56 | id: check-dependabot-config
57 | uses: actions/github-script@v7
58 | with:
59 | script: |
60 | const fs = require('fs');
61 |
62 | // Result object to store the message
63 | let result = {
64 | name: 'dependabot.yml',
65 | passed: true,
66 | message: ''
67 | }
68 |
69 | // Check that file exists
70 | if (!fs.existsSync('.github/dependabot.yml')) {
71 | result.passed = false;
72 | result.message = 'File is missing.';
73 | }
74 |
75 | return result;
76 |
77 | - name: Check for Security Policy
78 | id: check-security-policy
79 | uses: actions/github-script@v7
80 | with:
81 | script: |
82 | const fs = require('fs');
83 |
84 | // Result object to store the message
85 | let result = {
86 | name: 'SECURITY.md',
87 | passed: true,
88 | message: ''
89 | }
90 |
91 | // Check that file exists
92 | if (!fs.existsSync('SECURITY.md')) {
93 | result.passed = false;
94 | result.message = 'File is missing.';
95 | }
96 |
97 | return result;
98 |
99 | - name: Check all results
100 | id: check-all-results
101 | uses: actions/github-script@v7
102 | with:
103 | script: |
104 | const checks = [
105 | JSON.parse(process.env['check1']),
106 | JSON.parse(process.env['check2'])
107 | ];
108 |
109 | const result = checks.every(check => check.passed);
110 | return result
111 | env:
112 | check1: ${{ steps.check-dependabot-config.outputs.result }}
113 | check2: ${{ steps.check-security-policy.outputs.result }}
114 |
115 | - name: Build message - step results
116 | id: build-message-step-results
117 | uses: skills/action-text-variables@v2
118 | with:
119 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md
120 | template-vars: >
121 | {
122 | "step_number": 4,
123 | "passed": ${{ steps.check-all-results.outputs.result }},
124 | "results_table": [
125 | ${{ steps.check-dependabot-config.outputs.result }},
126 | ${{ steps.check-security-policy.outputs.result }}
127 | ]
128 | }
129 |
130 | - name: Create comment - step results
131 | run: |
132 | gh issue comment "$ISSUE_URL" \
133 | --body "$COMMENT_BODY" \
134 | --edit-last
135 | env:
136 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
137 | COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }}
138 |
139 | - name: Fail job if not all checks passed
140 | if: steps.check-all-results.outputs.result == 'false'
141 | run: exit 1
142 |
143 | # END: Check practical exercise
144 |
145 | - name: Build message - step finished
146 | id: build-message-step-finish
147 | uses: skills/action-text-variables@v2
148 | with:
149 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
150 | template-vars: |
151 | next_step_number: 5
152 |
153 | - name: Update comment - step finished
154 | run: |
155 | gh issue comment "$ISSUE_URL" \
156 | --body "$ISSUE_BODY"
157 | env:
158 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159 | ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
160 |
161 | post_next_step_content:
162 | name: Post next step content
163 | needs: [find_exercise, check_step_work]
164 | runs-on: ubuntu-latest
165 | env:
166 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
167 |
168 | steps:
169 | - name: Checkout
170 | uses: actions/checkout@v4
171 |
172 | - name: Get Exercise Toolkit
173 | uses: actions/checkout@v4
174 | with:
175 | repository: skills/exercise-toolkit
176 | path: exercise-toolkit
177 | ref: v0.6.0
178 |
179 | - name: Create comment - add step content
180 | run: |
181 | gh issue comment "$ISSUE_URL" \
182 | --body-file "$STEP_5_FILE"
183 | env:
184 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
185 |
186 | - name: Create comment - watching for progress
187 | run: |
188 | gh issue comment "$ISSUE_URL" \
189 | --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
190 | env:
191 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
192 |
193 | - name: Disable current workflow and enable next one
194 | run: |
195 | gh workflow disable "Step 4" || true
196 | gh workflow enable "Step 5" || true
197 | env:
198 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
199 |
--------------------------------------------------------------------------------
/.github/steps/2-prepare-to-collaborate.md:
--------------------------------------------------------------------------------
1 | # Step 2: Prepare to collaborate
2 |
3 | Your simple school website has become quite popular! After showing it at the last staff meeting, Ms. Rodriguez from the Art Club and Mr. Chen from the Chess Club came up to you super excited. They have tons of ideas for new features!
4 |
5 | - Ms. Rodriguez wants to add a photo gallery
6 | - Mr. Chen dreams of adding a tournament bracket system for the chess/sports activities! 🎨♟️
7 |
8 | While you're thrilled about their enthusiasm, you realize you need to set up some guidelines before letting them start changing code. The last thing you want is conflicting changes breaking the registration system right before spring break!
9 |
10 | Opening your project to other teachers at Mergington High means thinking about how everyone will collaborate together without breaking each other's code.
11 |
12 | **Collaborators** are the people you have granted write access to the project through repository settings.
13 |
14 | - Provide other people permissions to change project files while still protecting repository settings.
15 | - Personal repositories have simple permissions. Organization repositories allow flexible permissions such as read, write, maintain, and admin.
16 |
17 | To help with collaboration, GitHub provides two special files:
18 |
19 | 1. **`CONTRIBUTING.md` file** - A "How to Help" guide. Some example content:
20 |
21 | - How to prepare a developer setup of the extra-curricular activities website.
22 | - The process for suggesting changes.
23 | - The project's coding style preference, to keep things consistent.
24 | - How to ask for help when stuck.
25 |
26 | 1. **`CODEOWNERS` file** - Assign specific people or teams responsible for a portion of the project.
27 |
28 | - When someone creates a pull request, GitHub will automatically ask the right person to review it.
29 |
30 | ## ⌨️ Activity: Create a welcoming contribution guide
31 |
32 | The IT Club meeting is tomorrow, and you need to prepare for Ms. Rodriguez and Mr. Chen to join the project. Let's start a document to help them contribute effectively.
33 |
34 | 1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch.
35 |
36 |
37 |
38 | 1. In the top directory, create a new file called `CONTRIBUTING.md` (case sensitive).
39 |
40 | 1. Add a welcoming message.
41 |
42 | ```md
43 | # Contributing to the Mergington High Extra-Curricular Activities Website
44 |
45 | Thank you for your interest in helping improve our school's website!
46 | Whether you want to add your club's activities, fix a bug, or suggest
47 | new features, this guide will help you get started. 🎉
48 | ```
49 |
50 | 1. Add instructions to help teachers quickly start developing.
51 |
52 | ```md
53 | ## Development Setup
54 |
55 | 1. Clone the repository to your computer.
56 | 2. Install Python requirements: `pip install -r requirements.txt`.
57 | 3. Run the development server: `python src/app.py`.
58 | 4. Visit http://localhost:8000 in your browser to see the website.
59 |
60 | ## Making Changes
61 |
62 | 1. Create a new branch for your changes.
63 | - Use descriptive names like `art-gallery-feature` or `fix-chess-signup`
64 | 2. Make your changes and test them locally with sample student data.
65 | - Use the MongoDB extension to preview the included sample date.
66 | 3. Push your branch and create a pull request.
67 | 4. Wait for review and address any feedback.
68 |
69 | ## Code Style
70 |
71 | - Follow PEP 8 for Python code (backend).
72 | - Use clear, descriptive variable names (student_name, start_time, etc.)
73 | - Add comments to describe blocks of logic.
74 | ```
75 |
76 | 1. Add a section for getting help.
77 |
78 | ```md
79 | ## Need help or have ideas?
80 |
81 | - Check the open issues first.
82 | - If your problem is there, add a comment or up-vote.
83 | - If not there, create a new issue. Be as descriptive as possible.
84 | - Ask in our weekly IT Club office hours (Thursdays at lunch in Room 203).
85 | - For other general problems, email the tech team at techclub@mergingtonhigh.example.edu
86 | ```
87 |
88 | 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
89 |
90 | ## ⌨️ Activity: Assign code ownership
91 |
92 | With others joining the fun, you want to stay involved on anything affecting architecture and core functionality. Let's assign you to the related files.
93 |
94 | 1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch.
95 |
96 | 1. In the top directory, create a new file called `CODEOWNERS` (case sensitive and no extension).
97 |
98 | 1. Add the following content:
99 |
100 | ```codeowners
101 | # Core functionality - changes here should be rare!
102 | /src/app.py @{{ login }}
103 | /src/backend/database.py @{{ login }}
104 | /src/backend/routers/auth.py @{{ login }}
105 |
106 | # The frontend will need refactored soon to be more object oriented.
107 | /src/static/ @{{ login }}
108 | ```
109 |
110 | 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
111 |
112 | 1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
113 |
114 | ## ⌨️ Activity: (Optional) Add your first collaborator
115 |
116 | Ready to let your colleague start working on that photo gallery feature? Let's do it!
117 |
118 | > [!IMPORTANT]
119 | > This step is optional because it requires another person with a GitHub account to participate.
120 |
121 | 1. In the top navigation, select the **Settings** tab.
122 |
123 | 1. In the left navigation, select **Collaborators**.
124 |
125 | 1. Find the **Manage access** area and click the **Add people** button.
126 |
127 |
128 |
129 | 1. Enter a friend/colleague's GitHub username or email then press the **Add to repository** button.
130 |
131 |
132 |
133 | > [!IMPORTANT]
134 | > Personal repositories only have one collaboration role type. A "collaborator" receives **write** permissions but NOT **admin** permissions. If you need finer permissions, consider starting a free [organization](https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations) and assigning [repository roles](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization).
135 |
--------------------------------------------------------------------------------
/src/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Mergington High School Activities
7 |
8 |
13 |
14 |
15 |
16 | Mergington High School
17 | Extracurricular Activities
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
108 |
109 |
110 |
111 |
112 |
113 |
Loading activities...
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
124 |
125 |
126 |
127 |
128 |
×
129 |
Register for
130 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
×
150 |
Teacher Login
151 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/.github/steps/3-foster-healthy-growth.md:
--------------------------------------------------------------------------------
1 | # Step 3: Foster healthy growth
2 |
3 | With so many eager contributors, Principal Martinez pulled you aside after morning announcements: "Your website is becoming critical school infrastructure! We need to make sure it grows in a healthy way as more teachers join. Can you add some guidelines to keep everything organized?"
4 |
5 | As your extra-curricular activities website grows, you'll need more than just technical protections and contribution guides. You'll also have to encourage healthy and constructive communication.
6 |
7 | Let's look at a couple ways to do that:
8 |
9 | 1. **Code of Conduct** - This document sets expectations for how community members should interact. Think of it like the Student Handbook at Mergington High - it outlines respectful behavior, how to report non-technical problems, and consequences for violations.
10 |
11 | 2. **Issue Templates** - These provide structure when someone reports a problem or suggests a new feature. They can help the community effectively communicate their needs for new features and provide enough information to solve bugs.
12 |
13 | ## ⌨️ Activity: Set expectations with a Code of Conduct
14 |
15 | Let's start by establishing some community guidelines for your growing team of teacher-contributors.
16 |
17 | > [!TIP]
18 | > The [Contributor Covenant](https://www.contributor-covenant.org/) is a popular code of conduct used by many projects.
19 |
20 | 1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch.
21 |
22 | 1. In the top directory, create a new file called `CODE_OF_CONDUCT.md` (case sensitive).
23 |
24 | 1. Add the following content:
25 |
26 | ```markdown
27 | # Mergington High School Code of Conduct
28 |
29 | ## Our Pledge
30 |
31 | In the interest of fostering an open and welcoming environment for
32 | our school community, we as contributors and maintainers pledge to
33 | make participation in the Extra-Curricular Activities project a
34 | respectful and harassment-free experience for everyone.
35 |
36 | ## Our Standards
37 |
38 | Examples of behavior that contributes to creating a positive environment include:
39 |
40 | - Using welcoming and inclusive language
41 | - Being respectful of differing viewpoints and experiences
42 | - Gracefully accepting constructive criticism
43 | - Focusing on what is best for the students and the school community
44 | - Showing empathy towards other community members
45 |
46 | Examples of unacceptable behavior include:
47 |
48 | - The use of inappropriate language or imagery
49 | - Trolling, insulting comments, and personal attacks
50 | - Public or private harassment
51 | - Publishing others' private information without explicit permission
52 | - Other conduct which could reasonably be considered inappropriate in a school setting
53 |
54 | ## Responsibilities
55 |
56 | Project maintainers are responsible for clarifying the standards of
57 | acceptable behavior and are expected to take appropriate and fair
58 | corrective action in response to any instances of unacceptable behavior.
59 |
60 | Project maintainers have the right and responsibility to remove, edit,
61 | or reject comments, commits, code, issues, and other contributions that
62 | are not aligned to this Code of Conduct.
63 |
64 | ## Scope
65 |
66 | This Code of Conduct applies both within project spaces and in public spaces
67 | when an individual is representing the project or the school. Examples of
68 | representing the project include using an official project email address,
69 | posting via an official social media account, or acting as an appointed
70 | representative at an online or offline event.
71 |
72 | ## Enforcement
73 |
74 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
75 | reported to the IT Club faculty advisor. All complaints will be reviewed and
76 | investigated promptly and fairly.
77 |
78 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may
79 | face temporary or permanent repercussions as determined by the school administration.
80 |
81 | ## Attribution
82 |
83 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
84 | version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)
85 | ```
86 |
87 | 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
88 |
89 | ## ⌨️ Activity: Communicate easier with issue templates
90 |
91 | Now let's create templates so other teachers can report bugs or request features in a standardized way.
92 |
93 | > [!TIP]
94 | > You might consider trying the public preview for [issue forms](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms), which provide a friendlier user experience when creating issues.
95 |
96 | 1. In the top navigation, select the **Settings** tab.
97 |
98 | 1. Find the **Features** section and verify **Issues** is enabled.
99 |
100 |
101 |
102 | 1. Click the **Set up templates** button to enter the issue templates editor.
103 |
104 |
105 |
106 | 1. Click the **Add template** dropdown and select **Bug report**.
107 |
108 |
109 |
110 | 1. Click the **Preview and edit** button to show the current template. Click the **Edit icon** (pencil) to make the fields editable.
111 |
112 | 
113 |
114 | 
115 |
116 |
117 |
118 | 1. (Optional) Let's keep it simple for our students and fellow teachers. Remove the sections about Desktop and Smartphone details.
119 |
120 | 1. Repeat the above steps for the "Feature request" template.
121 |
122 |
123 |
124 | 1. With our templates prepared, let's commit them. In the top right, click the **Propose changes** button. Enter a description and set the branch to `add-issue-templates`, then click **Commit changes**. You can ignore the automatically created pull request.
125 |
126 |
127 |
128 | 1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
129 |
130 | > [!TIP]
131 | > Did you notice that you are working in parallel on 2 branches now? That's exactly what working with multiple collaborators is like.
132 |
--------------------------------------------------------------------------------
/.github/steps/4-prepare-for-the-inevitable.md:
--------------------------------------------------------------------------------
1 | # Step 4: Prepare for the inevitable
2 |
3 | As you settle into the teachers' lounge with your coffee, you realize something: With more and more teachers contributing to the code, it's only a matter of time before security vulnerabilities creep in. 😱
4 |
5 | Every codebase, no matter how well-maintained, will eventually face security challenges. Let's try to proactively prepare for that day by configuring a few tools GitHub offers:
6 |
7 | 1. **Dependabot** - Track and create alerts for vulnerabilities found in upstream dependencies used in your project. Automatically create pull requests to upgrade dependencies to safe versions.
8 |
9 | 1. **Code Scanning** - Analyze your repository's code to find security vulnerabilities and coding errors. Use GitHub Copilot Autofix to automatically suggest fixes for these alerts.
10 |
11 | 1. **Security Policy and Private vulnerability reporting** - Provide a guide and simple form for security researchers and end users to responsibly report vulnerabilities directly to the repository maintainer. This prevents sensitive issues from being publicly disclosed before they're fixed.
12 |
13 | > [!NOTE]
14 | > This is just a quick setup guide. For a more detailed setup of each service, we recommend the related GitHub Skills exercises and/or GitHub documentation.
15 |
16 | ## ⌨️ Activity: Automate security updates with Dependabot
17 |
18 | Let's configure Dependabot to use default settings and automatically combine fixes for open alerts, and create pull requests. This will allow us to stay up to date with very little overhead! Nice!
19 |
20 | > [!TIP]
21 | > For a deeper dive, check out the [Secure Repository Supply Chain](https://github.com/skills/secure-repository-supply-chain) Skills exercise!
22 |
23 | 1. In the top navigation, select the **Settings** tab.
24 |
25 | 1. In the left navigation, select **Advanced Security**.
26 |
27 | 1. Find the **Dependabot** section. Verify or change the settings to match the following:
28 |
29 | - **Dependabot alerts**: `enabled`
30 | - **Dependabot security updates**: `enabled`
31 | - **Grouped security updates**: `enabled`
32 |
33 | 1. Find **Dependabot version updates** and click the **Enable** button. This will open an editor to create a configuration file.
34 |
35 |
36 |
37 | 1. In the left files list, at the top, click the **Expand file tree** button to show the list of files. At the top, change the branch to `prepare-to-collaborate`. Remember, our ruleset won't let us directly change files on `main`.
38 |
39 |
40 |
41 | 1. Set the `package-ecosytem` to `pip` so Dependabot will automatically monitor our Python requirements.
42 |
43 |
44 |
45 | 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
46 |
47 | ## ⌨️ Activity: Detect dangerous patterns with code scanning
48 |
49 | None of us at the high school are professional software developers. Let's enable code scanning to alert us if we are potentially doing something unsafe. And, let's configure GitHub Copilot to create pull requests with solutions.
50 |
51 | > [!TIP]
52 | > Want to learn more about code scanning and writing custom queries? Check out the [Introduction to CodeQL](https://github.com/skills/introduction-to-codeql) Skills exercise after you finish this one!
53 |
54 | 1. In the top navigation, select the **Settings** tab.
55 |
56 | 1. In the left navigation, select **Advanced Security**.
57 |
58 | 1. Find the **Code scanning** section. Click the **Set up** button and select the **Default** option to open a configuration panel.
59 |
60 |
61 |
62 | 1. Click the **Enable CodeQL** button to accept the default configuration.
63 |
64 |
65 |
66 | 1. Below the **Tools** section. Verify **Copilot Autofix** is set to `On`.
67 |
68 |
69 |
70 | ## ⌨️ Activity: Provide a safe path for security findings
71 |
72 | Now that the automated options are ready, let's create a guide for real-life humans to report any security vulnerabilities they find in a safe way.
73 |
74 | 1. In the top navigation, select the **Settings** tab.
75 |
76 | 1. In the left navigation, select **Advanced Security**.
77 |
78 | 1. Find the **Private vulnerability reporting** setting and verify it is `enabled`.
79 |
80 | 1. At the top navigation, click the **Security** tab.
81 |
82 | 1. In the left navigation, click the **Policy** option.
83 |
84 | 1. Click the **Start setup** button. An editor will be started to create the file `SECURITY.md`.
85 |
86 | 
87 |
88 |
89 |
90 | 1. In the left files list, at the top, click the **Expand file tree** button to show the list of files. At the top, change the branch to `prepare-to-collaborate`. Remember, our ruleset won't let us directly change files on `main`.
91 |
92 | 1. We will ignore the provided template and instead use a recommendation from Mergington High School's IT department. Add the following content:
93 |
94 | > 💡**Tip** If you switch to a branch that does not contain the same file, the editor will become empty. Press the **Restore** button to retrieve the previous editor's content.
95 |
96 | ```markdown
97 | # Mergington High School Security Policy
98 |
99 | ## Reporting a Vulnerability
100 |
101 | At Mergington High, we take the security of our Extra-Curricular Activities website seriously, especially
102 | since it contains student information. If you discover a security vulnerability, please follow these steps:
103 |
104 | 1. **Do not** create an issue on this repository, disclose the vulnerability publicly, or discuss it with other teachers/students.
105 | 1. In the top navigation of this repository, click the **Security** tab.
106 | 1. In the top right, click the **Report a vulnerability** button.
107 | 1. Fill out the provided form. It will request information like:
108 | - A description of the vulnerability
109 | - Steps to reproduce the issue
110 | - Potential impact on student data or website functionality
111 | - Suggested fix (if you have one)
112 | 1. Email the IT Club faculty advisor at techsupport@mergingtonhigh.example.edu and inform them you have made a report. **Do not** include any vulnerability details.
113 |
114 | ## Response Timeline
115 |
116 | - We will acknowledge receipt of your report within 2 school days
117 | - We will provide an initial assessment within 5 school days
118 | - Critical issues affecting student data will be addressed immediately
119 | - We will create a private fork to solve the issue and invite you as a collaborator so you can see our progress and contribute.
120 |
121 | ## Thank You
122 |
123 | Your help in keeping our school's digital resources secure is greatly appreciated!
124 | Responsible disclosure of security vulnerabilities helps protect our entire school community.
125 | ```
126 |
127 | 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
128 |
129 | 1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
130 |
--------------------------------------------------------------------------------
/src/backend/database.py:
--------------------------------------------------------------------------------
1 | """
2 | MongoDB database configuration and setup for Mergington High School API
3 | """
4 |
5 | from pymongo import MongoClient
6 | from argon2 import PasswordHasher, exceptions as argon2_exceptions
7 |
8 | # Connect to MongoDB
9 | client = MongoClient('mongodb://localhost:27017/')
10 | db = client['mergington_high']
11 | activities_collection = db['activities']
12 | teachers_collection = db['teachers']
13 |
14 | # Methods
15 | def hash_password(password):
16 | """Hash password using Argon2"""
17 | ph = PasswordHasher()
18 | return ph.hash(password)
19 |
20 |
21 | def verify_password(hashed_password: str, plain_password: str) -> bool:
22 | """Verify a plain password against an Argon2 hashed password.
23 |
24 | Returns True when the password matches, False otherwise.
25 | """
26 | ph = PasswordHasher()
27 | try:
28 | ph.verify(hashed_password, plain_password)
29 | return True
30 | except argon2_exceptions.VerifyMismatchError:
31 | return False
32 | except Exception:
33 | # For any other exception (e.g., invalid hash), treat as non-match
34 | return False
35 |
36 | def init_database():
37 | """Initialize database if empty"""
38 |
39 | # Initialize activities if empty
40 | if activities_collection.count_documents({}) == 0:
41 | for name, details in initial_activities.items():
42 | activities_collection.insert_one({"_id": name, **details})
43 |
44 | # Initialize teacher accounts if empty
45 | if teachers_collection.count_documents({}) == 0:
46 | for teacher in initial_teachers:
47 | teachers_collection.insert_one({"_id": teacher["username"], **teacher})
48 |
49 | # Initial database if empty
50 | initial_activities = {
51 | "Chess Club": {
52 | "description": "Learn strategies and compete in chess tournaments",
53 | "schedule": "Mondays and Fridays, 3:15 PM - 4:45 PM",
54 | "schedule_details": {
55 | "days": ["Monday", "Friday"],
56 | "start_time": "15:15",
57 | "end_time": "16:45"
58 | },
59 | "max_participants": 12,
60 | "participants": ["michael@mergington.edu", "daniel@mergington.edu"]
61 | },
62 | "Programming Class": {
63 | "description": "Learn programming fundamentals and build software projects",
64 | "schedule": "Tuesdays and Thursdays, 7:00 AM - 8:00 AM",
65 | "schedule_details": {
66 | "days": ["Tuesday", "Thursday"],
67 | "start_time": "07:00",
68 | "end_time": "08:00"
69 | },
70 | "max_participants": 20,
71 | "participants": ["emma@mergington.edu", "sophia@mergington.edu"]
72 | },
73 | "Morning Fitness": {
74 | "description": "Early morning physical training and exercises",
75 | "schedule": "Mondays, Wednesdays, Fridays, 6:30 AM - 7:45 AM",
76 | "schedule_details": {
77 | "days": ["Monday", "Wednesday", "Friday"],
78 | "start_time": "06:30",
79 | "end_time": "07:45"
80 | },
81 | "max_participants": 30,
82 | "participants": ["john@mergington.edu", "olivia@mergington.edu"]
83 | },
84 | "Soccer Team": {
85 | "description": "Join the school soccer team and compete in matches",
86 | "schedule": "Tuesdays and Thursdays, 3:30 PM - 5:30 PM",
87 | "schedule_details": {
88 | "days": ["Tuesday", "Thursday"],
89 | "start_time": "15:30",
90 | "end_time": "17:30"
91 | },
92 | "max_participants": 22,
93 | "participants": ["liam@mergington.edu", "noah@mergington.edu"]
94 | },
95 | "Basketball Team": {
96 | "description": "Practice and compete in basketball tournaments",
97 | "schedule": "Wednesdays and Fridays, 3:15 PM - 5:00 PM",
98 | "schedule_details": {
99 | "days": ["Wednesday", "Friday"],
100 | "start_time": "15:15",
101 | "end_time": "17:00"
102 | },
103 | "max_participants": 15,
104 | "participants": ["ava@mergington.edu", "mia@mergington.edu"]
105 | },
106 | "Art Club": {
107 | "description": "Explore various art techniques and create masterpieces",
108 | "schedule": "Thursdays, 3:15 PM - 5:00 PM",
109 | "schedule_details": {
110 | "days": ["Thursday"],
111 | "start_time": "15:15",
112 | "end_time": "17:00"
113 | },
114 | "max_participants": 15,
115 | "participants": ["amelia@mergington.edu", "harper@mergington.edu"]
116 | },
117 | "Drama Club": {
118 | "description": "Act, direct, and produce plays and performances",
119 | "schedule": "Mondays and Wednesdays, 3:30 PM - 5:30 PM",
120 | "schedule_details": {
121 | "days": ["Monday", "Wednesday"],
122 | "start_time": "15:30",
123 | "end_time": "17:30"
124 | },
125 | "max_participants": 20,
126 | "participants": ["ella@mergington.edu", "scarlett@mergington.edu"]
127 | },
128 | "Math Club": {
129 | "description": "Solve challenging problems and prepare for math competitions",
130 | "schedule": "Tuesdays, 7:15 AM - 8:00 AM",
131 | "schedule_details": {
132 | "days": ["Tuesday"],
133 | "start_time": "07:15",
134 | "end_time": "08:00"
135 | },
136 | "max_participants": 10,
137 | "participants": ["james@mergington.edu", "benjamin@mergington.edu"]
138 | },
139 | "Debate Team": {
140 | "description": "Develop public speaking and argumentation skills",
141 | "schedule": "Fridays, 3:30 PM - 5:30 PM",
142 | "schedule_details": {
143 | "days": ["Friday"],
144 | "start_time": "15:30",
145 | "end_time": "17:30"
146 | },
147 | "max_participants": 12,
148 | "participants": ["charlotte@mergington.edu", "amelia@mergington.edu"]
149 | },
150 | "Weekend Robotics Workshop": {
151 | "description": "Build and program robots in our state-of-the-art workshop",
152 | "schedule": "Saturdays, 10:00 AM - 2:00 PM",
153 | "schedule_details": {
154 | "days": ["Saturday"],
155 | "start_time": "10:00",
156 | "end_time": "14:00"
157 | },
158 | "max_participants": 15,
159 | "participants": ["ethan@mergington.edu", "oliver@mergington.edu"]
160 | },
161 | "Science Olympiad": {
162 | "description": "Weekend science competition preparation for regional and state events",
163 | "schedule": "Saturdays, 1:00 PM - 4:00 PM",
164 | "schedule_details": {
165 | "days": ["Saturday"],
166 | "start_time": "13:00",
167 | "end_time": "16:00"
168 | },
169 | "max_participants": 18,
170 | "participants": ["isabella@mergington.edu", "lucas@mergington.edu"]
171 | },
172 | "Sunday Chess Tournament": {
173 | "description": "Weekly tournament for serious chess players with rankings",
174 | "schedule": "Sundays, 2:00 PM - 5:00 PM",
175 | "schedule_details": {
176 | "days": ["Sunday"],
177 | "start_time": "14:00",
178 | "end_time": "17:00"
179 | },
180 | "max_participants": 16,
181 | "participants": ["william@mergington.edu", "jacob@mergington.edu"]
182 | }
183 | }
184 |
185 | initial_teachers = [
186 | {
187 | "username": "mrodriguez",
188 | "display_name": "Ms. Rodriguez",
189 | "password": hash_password("art123"),
190 | "role": "teacher"
191 | },
192 | {
193 | "username": "mchen",
194 | "display_name": "Mr. Chen",
195 | "password": hash_password("chess456"),
196 | "role": "teacher"
197 | },
198 | {
199 | "username": "principal",
200 | "display_name": "Principal Martinez",
201 | "password": hash_password("admin789"),
202 | "role": "admin"
203 | }
204 | ]
205 |
206 |
--------------------------------------------------------------------------------
/.github/workflows/3-foster-healthy-growth.yml:
--------------------------------------------------------------------------------
1 | name: Step 3
2 |
3 | on:
4 | push:
5 | branches:
6 | - add-issue-templates
7 | - prepare-to-collaborate
8 | paths:
9 | - ".github/ISSUE_TEMPLATE/**"
10 | - "CODE_OF_CONDUCT.md"
11 |
12 | permissions:
13 | contents: read
14 | actions: write
15 | issues: write
16 |
17 | env:
18 | STEP_4_FILE: ".github/steps/4-prepare-for-the-inevitable.md"
19 |
20 | jobs:
21 | find_exercise:
22 | if: |
23 | !github.event.repository.is_template
24 | name: Find Exercise Issue
25 | uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.6.0
26 |
27 | check_step_work:
28 | name: Check step work
29 | runs-on: ubuntu-latest
30 | needs: [find_exercise]
31 | env:
32 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
33 |
34 | steps:
35 | - name: Checkout branch - prepare-to-collaborate
36 | uses: actions/checkout@v4
37 | continue-on-error: true
38 | with:
39 | path: prepare-to-collaborate
40 | ref: prepare-to-collaborate
41 |
42 | - name: Checkout branch - add-issue-templates
43 | uses: actions/checkout@v4
44 | continue-on-error: true
45 | with:
46 | path: add-issue-templates
47 | ref: add-issue-templates
48 |
49 | - name: Get Exercise Toolkit
50 | uses: actions/checkout@v4
51 | with:
52 | repository: skills/exercise-toolkit
53 | path: exercise-toolkit
54 | # Results table still uses old format. Needs refactored to update.
55 | ref: v0.3.0
56 |
57 | - name: Update comment - checking work
58 | run: |
59 | gh issue comment "$ISSUE_URL" \
60 | --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
61 | --edit-last
62 | env:
63 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64 |
65 | # START: Check practical exercise
66 |
67 | - name: Check for Code of Conduct
68 | id: check-code-of-conduct
69 | uses: actions/github-script@v7
70 | with:
71 | script: |
72 | const fs = require('fs');
73 |
74 | // Result object to store the message
75 | let result = {
76 | name: 'CODE_OF_CONDUCT.md',
77 | passed: true,
78 | message: ''
79 | }
80 |
81 | // Check that file exists
82 | if (!fs.existsSync('prepare-to-collaborate/CODE_OF_CONDUCT.md')) {
83 | result.passed = false;
84 | result.message = 'File is missing.';
85 | }
86 |
87 | return result;
88 |
89 | - name: Check for Bug Report Templates
90 | id: check-bug-report-template
91 | uses: actions/github-script@v7
92 | with:
93 | script: |
94 | const fs = require('fs');
95 |
96 | // Result object to store the message
97 | let result = {
98 | name: 'bug_report.md',
99 | passed: true,
100 | message: ''
101 | }
102 |
103 | // Check that file exists
104 | if (!fs.existsSync('add-issue-templates/.github/ISSUE_TEMPLATE/bug_report.md')) {
105 | result.passed = false;
106 | result.message = 'File is missing.';
107 | }
108 |
109 | return result;
110 |
111 | - name: Check for Feature Request Templates
112 | id: check-feature-request-template
113 | uses: actions/github-script@v7
114 | continue-on-error: true
115 | with:
116 | script: |
117 | const fs = require('fs');
118 |
119 | // Result object to store the message
120 | let result = {
121 | name: 'feature_request.md',
122 | passed: true,
123 | message: ''
124 | }
125 |
126 | // Check that file exists
127 | if (!fs.existsSync('add-issue-templates/.github/ISSUE_TEMPLATE/feature_request.md')) {
128 | result.passed = false;
129 | result.message = 'File is missing.';
130 | }
131 |
132 | return result;
133 |
134 | - name: Check all results
135 | id: check-all-results
136 | uses: actions/github-script@v7
137 | with:
138 | script: |
139 | const checks = [
140 | JSON.parse(process.env['check1']),
141 | JSON.parse(process.env['check2']),
142 | JSON.parse(process.env['check3'])
143 | ];
144 |
145 | const result = checks.every(check => check.passed);
146 | return result
147 | env:
148 | check1: ${{ steps.check-code-of-conduct.outputs.result }}
149 | check2: ${{ steps.check-bug-report-template.outputs.result }}
150 | check3: ${{ steps.check-feature-request-template.outputs.result }}
151 |
152 | - name: Build message - step results
153 | id: build-message-step-results
154 | uses: skills/action-text-variables@v2
155 | with:
156 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md
157 | template-vars: >
158 | {
159 | "step_number": 3,
160 | "passed": ${{ steps.check-all-results.outputs.result }},
161 | "results_table": [
162 | ${{ steps.check-code-of-conduct.outputs.result }},
163 | ${{ steps.check-bug-report-template.outputs.result }},
164 | ${{ steps.check-feature-request-template.outputs.result }}
165 | ]
166 | }
167 |
168 | - name: Create comment - step results
169 | run: |
170 | gh issue comment "$ISSUE_URL" \
171 | --body "$COMMENT_BODY" \
172 | --edit-last
173 | env:
174 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
175 | COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }}
176 |
177 | - name: Fail job if not all checks passed
178 | if: steps.check-all-results.outputs.result == 'false'
179 | run: exit 1
180 |
181 | # END: Check practical exercise
182 |
183 | - name: Build message - step finished
184 | id: build-message-step-finish
185 | uses: skills/action-text-variables@v2
186 | with:
187 | template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
188 | template-vars: |
189 | next_step_number: 4
190 |
191 | - name: Update comment - step finished
192 | run: |
193 | gh issue comment "$ISSUE_URL" \
194 | --body "$ISSUE_BODY"
195 | env:
196 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
197 | ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
198 |
199 | post_next_step_content:
200 | name: Post next step content
201 | needs: [find_exercise, check_step_work]
202 | runs-on: ubuntu-latest
203 | env:
204 | ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
205 |
206 | steps:
207 | - name: Checkout
208 | uses: actions/checkout@v4
209 |
210 | - name: Get Exercise Toolkit
211 | uses: actions/checkout@v4
212 | with:
213 | repository: skills/exercise-toolkit
214 | path: exercise-toolkit
215 | ref: v0.6.0
216 |
217 | - name: Create comment - add step content
218 | run: |
219 | gh issue comment "$ISSUE_URL" \
220 | --body-file "$STEP_4_FILE"
221 | env:
222 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
223 |
224 | - name: Create comment - watching for progress
225 | run: |
226 | gh issue comment "$ISSUE_URL" \
227 | --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
228 | env:
229 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
230 |
231 | - name: Disable current workflow and enable next one
232 | run: |
233 | gh workflow disable "Step 3" || true
234 | gh workflow enable "Step 4" || true
235 | env:
236 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
237 |
--------------------------------------------------------------------------------
/.github/steps/1-protect-your-code.md:
--------------------------------------------------------------------------------
1 | # Step 1: Protect your code
2 |
3 | It's been a busy month at Mergington High! Your simple website for managing extra-curricular activities has really taken off. What started as a basic sign-up form for a few activities has grown into the go-to place for half the school activities. 📚✨
4 |
5 | Principal Martinez was so impressed with your work that they announced at the last staff meeting that ALL clubs should start using the website. While this is exciting, you're a bit nervous - the last thing you want is an accidental change breaking the system right before the big Fall Activities Fair! 😰
6 |
7 | When more teachers start helping with the Mergington High activities website, it's important to add some safeguards. Thankfully, GitHub provides several ways to protect your repository:
8 |
9 | 1. **Repository Rulesets** - These provide safeguards to limit:
10 |
11 | - Pushing code directly to important branches
12 | - Deleting or renaming branches
13 | - Force pushing (which can overwrite history)
14 | - (and much more)
15 |
16 | 1. **`.gitignore`** - This special file tells Git which files it should NOT track, like:
17 |
18 | - Temporary files that your code creates while running
19 | - Secret configuration files with sensitive information
20 | - System files that other developers don't need
21 |
22 | > [!TIP]
23 | > Think of these settings like the editorial process of a school yearbook. Various student committees will take photos and write articles, then the yearbook president will make adjustments to make sure everything flows together properly. Finally, a teacher/advisor will sign off that all content is appropriate.
24 |
25 | ## ⌨️ Activity: (optional) Get to know our extracurricular activities site
26 |
27 |
28 | Show Steps
29 |
30 | In [Getting Started with GitHub Copilot](https://github.com/skills/getting-started-with-github-copilot/) exercise, we have been developing the Extracurricular activities website. You can follow these steps to start up the development environment and try it out.
31 |
32 | > ❗ **Important:** Opening a development environment and running the application is **NOT** necessary to complete this exercise. You can skip this activity if desired.
33 |
34 | 1. Right-click the below button to open the **Create Codespace** page in a new tab. Use the default configuration.
35 |
36 | [](https://codespaces.new/{{full_repo_name}}?quickstart=1)
37 |
38 | 1. Wait some time for the environment to be prepared. It will automatically install all requirements and services.
39 |
40 | 1. Validate the **GitHub Copilot** and **Python** extensions are installed and enabled.
41 |
42 | 
43 |
44 |
45 | 1. Try running the application. In the left sidebar, select the **Run and Debug** tab and then press the **Start Debugging** icon.
46 |
47 |
48 | 📸 Show screenshot
49 |
50 |
51 |
52 |
53 |
54 |
55 | 🤷 Having trouble?
56 |
57 | If the **Run and Debug** area is empty, try reloading VS Code: Open the command palette (`Ctrl`+`Shift`+`P`) and search for `Developer: Reload Window`.
58 |
59 |
60 |
61 |
62 |
63 | 1. Use the **Ports** tab to find the webpage address, open it, and verify it is running.
64 |
65 |
66 | 📸 Show screenshot
67 |
68 |
69 |
70 | 
71 |
72 |
73 |
74 |
75 |
76 | ## ⌨️ Activity: Add a branch ruleset
77 |
78 | To get started, let's add some protections so that no one accidentally breaks the club registration system.
79 |
80 | 1. If necessary, open another tab and navigate to this repository. We will start on the **Settings** tab.
81 |
82 | 1. In the left navigation, expand the **Rules** area and select **Rulesets**.
83 |
84 | 1. Click the **New ruleset** dropdown and select **New branch ruleset**.
85 |
86 |
87 |
88 | 1. Set the **Ruleset Name** as `Protect main` and change the **Enforcement status** to `Active`.
89 |
90 |
91 |
92 | 1. Find the **Targets** section and use the **Add target** dropdown to add 2 entries:
93 |
94 | 1. Add the **Include default branch** option to ensure protections aren't bypassed by switching the default branch.
95 |
96 |
97 |
98 | 1. Use the **include by pattern** option and enter the pattern `main`.
99 |
100 |
101 |
102 |
103 |
104 | 1. Find the **Rules** section and ensure the following items are checked.
105 |
106 | - [x] Restrict deletions
107 | - [x] Require a pull request before merging
108 | - Required approvals: `0`
109 | - [x] Require review from Code Owners
110 | - [x] Block force pushes
111 |
112 | 1. Scroll to the bottom and click the **Create** button to save the ruleset.
113 |
114 | ## ⌨️ Activity: Create a `.gitignore` file
115 |
116 | We know many teachers use different tools, so let's make sure they don't accidentally commit unnecessary files.
117 |
118 | 1. At the top navigation, return to the **Code** tab and verify you are on the `main` branch.
119 |
120 | 1. Above the list of files, click the **Add file** dropdown and select **Create new file**.
121 |
122 |
123 |
124 | 1. Enter the file name `.gitignore`. We will ignore the template selector for now and make our own. Copy the below example content into it.
125 |
126 |
127 |
128 | ```gitignore
129 | # Python backend for club management
130 | __pycache__/
131 | *.py[cod] # Python compiled files
132 | *$py.class
133 | *.so
134 | .Python
135 | env/
136 | .env # Where database passwords are stored
137 | venv/ # Virtual environment for testing
138 | .venv
139 |
140 | # Teacher IDE settings
141 | .vscode/ # Ms. Rodriguez uses VS Code
142 | .idea/ # Mr. Chen uses PyCharm
143 |
144 | # Local development & testing
145 | instance/
146 | .pytest_cache/
147 | .coverage # Test coverage reports
148 | htmlcov/
149 |
150 | # Staff computer files
151 | .DS_Store # For teachers with Macs
152 | Thumbs.db # For teachers with Windows
153 | ```
154 |
155 | 1. In the top right, select the **Commit changes...** button. Notice that it won't let us commit to the `main` branch! Our ruleset is working! Nice!
156 |
157 |
158 |
159 | 1. Enter `prepare-to-collaborate` for the branch name then click the **Propose changes** button. You will be forwarded to a new page to start a new pull request.
160 |
161 | 1. Set the title to `Prepare to collaborate` and click the **Create pull request** button. **Do NOT merge yet**, since we will be adding more collaboration related changes.
162 |
163 | 1. With the file committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
164 |
165 | > [!TIP]
166 | > GitHub and the community have built a repository with [sample `.gitignore` files](https://github.com/github/gitignore) for many situations. Make sure to check it out!
167 |
168 |
169 | 🤷 Having trouble?
170 |
171 | Make sure you pushed the `.gitignore` file to `prepare-to-collaborate` branch. Exact naming for both matters!
172 |
173 |
174 |
--------------------------------------------------------------------------------
/src/static/styles.css:
--------------------------------------------------------------------------------
1 | /* Color palette */
2 | :root {
3 | /* Primary colors */
4 | --primary: #1a237e;
5 | --primary-light: #534bae;
6 | --primary-dark: #000051;
7 | --primary-text: #ffffff;
8 |
9 | /* Secondary colors */
10 | --secondary: #ff6f00;
11 | --secondary-light: #ffa040;
12 | --secondary-dark: #c43e00;
13 | --secondary-text: #ffffff;
14 |
15 | /* Neutral colors */
16 | --background: #f5f5f5;
17 | --surface: #ffffff;
18 | --text-primary: #333333;
19 | --text-secondary: #666666;
20 | --border: #e0e0e0;
21 | --border-light: #f0f0f0;
22 | --border-focus: #d0d0d0;
23 |
24 | /* Feedback colors */
25 | --success: #2e7d32;
26 | --success-light: #e8f5e9;
27 | --warning: #ff9800;
28 | --warning-light: #fff3cd;
29 | --error: #c62828;
30 | --error-light: #ffebee;
31 | --info: #0c5460;
32 | --info-light: #d1ecf1;
33 | }
34 |
35 | * {
36 | box-sizing: border-box;
37 | margin: 0;
38 | padding: 0;
39 | font-family: Arial, sans-serif;
40 | }
41 |
42 | body {
43 | font-family: Arial, sans-serif;
44 | line-height: 1.4;
45 | color: var(--text-primary);
46 | max-width: 1200px;
47 | margin: 0 auto;
48 | padding: 12px;
49 | background-color: var(--background);
50 | font-size: 0.9rem;
51 | }
52 |
53 | header {
54 | text-align: center;
55 | padding: 12px 0;
56 | margin-bottom: 15px;
57 | background-color: var(--primary);
58 | color: var(--primary-text);
59 | border-radius: 5px;
60 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
61 | position: relative;
62 | display: flex;
63 | flex-direction: column;
64 | align-items: center;
65 | }
66 |
67 | header h1 {
68 | margin-bottom: 5px;
69 | font-size: 1.6rem;
70 | }
71 |
72 | header h2 {
73 | font-size: 1.2rem;
74 | }
75 |
76 | main {
77 | display: flex;
78 | flex-wrap: wrap;
79 | justify-content: center;
80 | }
81 |
82 | section {
83 | background-color: var(--surface);
84 | padding: 15px;
85 | border-radius: 5px;
86 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
87 | width: 100%;
88 | }
89 |
90 | section h3 {
91 | margin-bottom: 15px;
92 | padding-bottom: 8px;
93 | border-bottom: 1px solid var(--border);
94 | color: var(--primary);
95 | font-size: 1.1rem;
96 | }
97 |
98 | /* New Layout Styles */
99 | .main-content-layout {
100 | display: flex;
101 | flex-direction: column;
102 | gap: 15px;
103 | }
104 |
105 | .sidebar-filters {
106 | padding: 15px;
107 | background-color: var(--surface);
108 | border-radius: 5px;
109 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
110 | }
111 |
112 | .sidebar-filters h3 {
113 | margin-bottom: 15px;
114 | padding-bottom: 8px;
115 | border-bottom: 1px solid var(--border);
116 | color: var(--primary);
117 | font-size: 1.1rem;
118 | }
119 |
120 | .activities-content {
121 | flex: 1;
122 | }
123 |
124 | /* Desktop layout */
125 | @media (min-width: 768px) {
126 | .main-content-layout {
127 | flex-direction: row;
128 | align-items: flex-start;
129 | }
130 |
131 | .sidebar-filters {
132 | width: 200px; /* Reduced from 250px to 200px to make the sidebar narrower */
133 | position: sticky;
134 | top: 15px;
135 | max-height: calc(100vh - 30px);
136 | overflow-y: auto;
137 | }
138 |
139 | .activities-content {
140 | flex: 1;
141 | }
142 | }
143 |
144 | #activities-list {
145 | display: grid;
146 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
147 | gap: 15px;
148 | width: 100%;
149 | }
150 |
151 | .activity-card {
152 | padding: 12px;
153 | border: 1px solid var(--border);
154 | border-radius: 6px;
155 | background-color: var(--surface);
156 | display: flex;
157 | flex-direction: column;
158 | height: 100%;
159 | transition: all 0.3s ease;
160 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
161 | position: relative;
162 | overflow: hidden;
163 | font-size: 0.85rem;
164 | }
165 |
166 | .activity-card:hover {
167 | transform: translateY(-5px);
168 | box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
169 | border-color: var(--border-focus);
170 | }
171 |
172 | .activity-card h4 {
173 | margin-bottom: 8px;
174 | color: var(--primary);
175 | font-size: 1rem;
176 | letter-spacing: 0.3px;
177 | padding-bottom: 6px;
178 | border-bottom: 1px solid var(--border-light);
179 | }
180 |
181 | .activity-card p {
182 | margin-bottom: 8px;
183 | line-height: 1.4;
184 | }
185 |
186 | .activity-card-actions {
187 | margin-top: auto;
188 | padding-top: 10px;
189 | display: flex;
190 | justify-content: center;
191 | }
192 |
193 | /* Activity Tag */
194 | .activity-tag {
195 | position: absolute;
196 | top: 8px;
197 | right: 8px;
198 | background: #e8eaf6;
199 | color: #3949ab;
200 | font-size: 0.65rem;
201 | font-weight: bold;
202 | padding: 2px 6px;
203 | border-radius: 10px;
204 | text-transform: uppercase;
205 | letter-spacing: 0.3px;
206 | }
207 |
208 | /* Capacity Indicator */
209 | .capacity-container {
210 | margin: 8px 0;
211 | width: 100%;
212 | }
213 |
214 | .capacity-bar-bg {
215 | height: 6px;
216 | background-color: var(--border-light);
217 | border-radius: 3px;
218 | overflow: hidden;
219 | }
220 |
221 | .capacity-text {
222 | display: flex;
223 | justify-content: space-between;
224 | margin-top: 3px;
225 | font-size: 0.7rem;
226 | color: var(--text-secondary);
227 | }
228 |
229 | .capacity-full .capacity-bar-fill {
230 | background-color: var(--error);
231 | }
232 |
233 | .capacity-near-full .capacity-bar-fill {
234 | background-color: var(--warning);
235 | }
236 |
237 | .capacity-available .capacity-bar-fill {
238 | background-color: var(--success);
239 | }
240 |
241 | /* Participants list */
242 | .participants-list {
243 | margin-top: 8px;
244 | padding-top: 8px;
245 | border-top: 1px solid var(--border-light);
246 | }
247 |
248 | .participants-list h5 {
249 | color: var(--primary);
250 | margin-bottom: 5px;
251 | font-size: 0.8em;
252 | }
253 |
254 | .participants-list ul {
255 | list-style-type: none;
256 | padding-left: 0;
257 | margin: 0;
258 | max-height: 100px;
259 | }
260 |
261 | .participants-list li {
262 | padding: 2px 0;
263 | color: var(--text-secondary);
264 | font-size: 0.8em;
265 | display: flex;
266 | justify-content: space-between;
267 | align-items: center;
268 | }
269 |
270 | /* Buttons */
271 | .register-button {
272 | background: linear-gradient(145deg, var(--secondary), var(--secondary-dark));
273 | color: var(--secondary-text);
274 | margin-top: 10px;
275 | padding: 6px 12px;
276 | width: 100%;
277 | font-weight: bold;
278 | border-radius: 20px;
279 | box-shadow: 0 2px 4px rgba(255, 111, 0, 0.2);
280 | transition: all 0.3s ease;
281 | display: flex;
282 | justify-content: center;
283 | align-items: center;
284 | font-size: 0.85rem;
285 | letter-spacing: 0.3px;
286 | text-transform: uppercase;
287 | border: none;
288 | }
289 |
290 | button {
291 | background-color: var(--primary);
292 | color: white;
293 | border: none;
294 | padding: 6px 12px;
295 | font-size: 0.85rem;
296 | border-radius: 4px;
297 | cursor: pointer;
298 | transition: background-color 0.2s;
299 | }
300 |
301 | /* Tooltip styles */
302 | .tooltip {
303 | position: relative;
304 | display: inline-block;
305 | cursor: pointer;
306 | }
307 |
308 | .tooltip .tooltip-text {
309 | visibility: hidden;
310 | background-color: rgba(33, 33, 33, 0.9);
311 | color: #fff;
312 | text-align: center;
313 | padding: 8px 12px;
314 | border-radius: 4px;
315 | font-size: 0.8rem;
316 | position: absolute;
317 | z-index: 1;
318 | bottom: 125%;
319 | left: 50%;
320 | transform: translateX(-50%);
321 | opacity: 0;
322 | transition: opacity 0.2s, visibility 0.2s;
323 | width: max-content;
324 | max-width: 250px;
325 | pointer-events: none;
326 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
327 | }
328 |
329 | .tooltip:hover .tooltip-text {
330 | visibility: visible;
331 | opacity: 1;
332 | }
333 |
334 | /* Special positioning for delete participant tooltip */
335 | .delete-participant {
336 | cursor: pointer;
337 | }
338 |
339 | .delete-participant .tooltip-text {
340 | left: auto;
341 | right: calc(100% + 8px);
342 | top: 50%;
343 | bottom: auto;
344 | transform: translateY(-50%);
345 | white-space: nowrap;
346 | }
347 |
348 | /* Activity skeletons for loading state */
349 | .skeleton-card {
350 | padding: 12px;
351 | border: 1px solid var(--border);
352 | border-radius: 6px;
353 | background-color: var(--surface);
354 | display: flex;
355 | flex-direction: column;
356 | height: 180px;
357 | position: relative;
358 | overflow: hidden;
359 | }
360 |
361 | .skeleton-line {
362 | height: 10px;
363 | margin-bottom: 8px;
364 | background: linear-gradient(
365 | 90deg,
366 | var(--border-light) 25%,
367 | var(--border) 50%,
368 | var(--border-light) 75%
369 | );
370 | background-size: 200% 100%;
371 | border-radius: 3px;
372 | animation: shimmer 1.5s infinite;
373 | }
374 |
375 | .skeleton-title {
376 | height: 18px;
377 | width: 70%;
378 | margin-bottom: 10px;
379 | }
380 |
381 | .skeleton-text {
382 | width: 100%;
383 | }
384 |
385 | .skeleton-text.short {
386 | width: 60%;
387 | }
388 |
389 | @keyframes shimmer {
390 | 0% {
391 | background-position: -200% 0;
392 | }
393 | 100% {
394 | background-position: 200% 0;
395 | }
396 | }
397 |
398 | /* Modal animation */
399 | .modal {
400 | position: fixed;
401 | top: 0;
402 | left: 0;
403 | width: 100%;
404 | height: 100%;
405 | background-color: rgba(0, 0, 0, 0.5);
406 | display: flex;
407 | justify-content: center;
408 | align-items: center;
409 | z-index: 1000;
410 | opacity: 0;
411 | transition: opacity 0.3s ease;
412 | }
413 |
414 | .modal.show {
415 | opacity: 1;
416 | }
417 |
418 | .modal-content {
419 | background-color: var(--surface);
420 | padding: 18px;
421 | border-radius: 5px;
422 | width: 90%;
423 | max-width: 350px;
424 | position: relative;
425 | transform: translateY(-20px);
426 | transition: transform 0.3s ease;
427 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
428 | border: 1px solid var(--border);
429 | }
430 |
431 | .modal.show .modal-content {
432 | transform: translateY(0);
433 | }
434 |
435 | .close-modal,
436 | .close-login-modal {
437 | position: absolute;
438 | right: 12px;
439 | top: 8px;
440 | font-size: 18px;
441 | cursor: pointer;
442 | color: var(--text-secondary);
443 | line-height: 1;
444 | }
445 |
446 | /* Login modal specific styling */
447 | #login-modal .modal-content h3 {
448 | color: var(--primary);
449 | font-size: 1.2rem;
450 | margin-bottom: 15px;
451 | text-align: center;
452 | }
453 |
454 | /* Hidden class - critical for showing/hiding elements */
455 | .hidden {
456 | display: none !important;
457 | }
458 |
459 | /* More compact no-results message */
460 | .no-results {
461 | text-align: center;
462 | padding: 12px;
463 | color: var(--text-secondary);
464 | background-color: var(--surface);
465 | border-radius: 6px;
466 | border: 1px dashed var(--border);
467 | margin: 10px 0;
468 | font-size: 0.85rem;
469 | }
470 |
471 | footer {
472 | text-align: center;
473 | margin-top: 20px;
474 | padding: 10px;
475 | color: var(--text-secondary);
476 | font-size: 0.8rem;
477 | }
478 |
479 | /* Search and Filter Components - Updated for Sidebar */
480 | .search-box {
481 | display: flex;
482 | width: 100%;
483 | margin-bottom: 15px;
484 | }
485 |
486 | .search-box input {
487 | flex: 1;
488 | min-width: 0; /* Add this to prevent input from overflowing */
489 | padding: 6px 10px;
490 | border: 1px solid var(--border);
491 | border-right: none;
492 | border-radius: 20px 0 0 20px;
493 | font-size: 0.85rem;
494 | transition: all 0.3s ease;
495 | }
496 |
497 | .search-box input:focus {
498 | outline: none;
499 | border-color: var(--primary-light);
500 | box-shadow: 0 0 0 2px rgba(83, 75, 174, 0.1);
501 | }
502 |
503 | .search-box button {
504 | width: 36px;
505 | height: 100%; /* Make button match input height */
506 | min-height: 32px; /* Ensure minimum clickable area */
507 | background: var(--primary);
508 | color: white;
509 | border: none;
510 | border-radius: 0 20px 20px 0;
511 | cursor: pointer;
512 | transition: background-color 0.3s ease;
513 | padding: 0;
514 | display: flex;
515 | align-items: center;
516 | justify-content: center;
517 | }
518 |
519 | .search-box button:hover {
520 | background-color: var(--primary-light);
521 | }
522 |
523 | .search-icon {
524 | font-size: 1rem;
525 | }
526 |
527 | .filter-container {
528 | margin-bottom: 15px;
529 | display: flex;
530 | flex-direction: column;
531 | gap: 6px;
532 | }
533 |
534 | .filter-label {
535 | font-weight: 600;
536 | color: var(--text-primary);
537 | font-size: 0.8rem;
538 | }
539 |
540 | .category-filters,
541 | .day-filters,
542 | .time-filters {
543 | display: flex;
544 | flex-wrap: wrap;
545 | gap: 6px;
546 | }
547 |
548 | .category-filter,
549 | .day-filter,
550 | .time-filter {
551 | background-color: var(--background);
552 | border: 1px solid var(--border);
553 | color: var(--text-primary);
554 | padding: 4px 10px;
555 | border-radius: 15px;
556 | font-size: 0.75rem;
557 | cursor: pointer;
558 | transition: all 0.2s ease;
559 | }
560 |
561 | .category-filter.active,
562 | .day-filter.active,
563 | .time-filter.active {
564 | background-color: var(--primary);
565 | color: white;
566 | border-color: var(--primary-dark);
567 | }
568 |
569 | .category-filter:hover,
570 | .day-filter:hover,
571 | .time-filter:hover {
572 | background-color: var(--primary-light);
573 | color: white;
574 | }
575 |
576 | .time-filters {
577 | width: 100%;
578 | }
579 |
580 | .reset-button {
581 | background-color: var(--border);
582 | color: var(--text-primary);
583 | padding: 5px 12px;
584 | border-radius: 15px;
585 | font-size: 0.8rem;
586 | font-weight: 500;
587 | cursor: pointer;
588 | transition: all 0.2s ease;
589 | align-self: flex-start;
590 | }
591 |
592 | .reset-button:hover {
593 | background-color: var(--border-focus);
594 | }
595 |
596 | /* Responsive adjustments for mobile */
597 | @media (max-width: 767px) {
598 | .sidebar-filters {
599 | margin-bottom: 15px;
600 | }
601 |
602 | .main-content-layout {
603 | flex-direction: column;
604 | }
605 |
606 | #activities-list {
607 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
608 | }
609 |
610 | .sidebar-filters {
611 | width: 100%;
612 | }
613 | }
614 |
615 | /* User controls in header */
616 | #user-controls {
617 | position: absolute;
618 | top: 10px;
619 | right: 15px;
620 | }
621 |
622 | #user-status {
623 | display: flex;
624 | align-items: center;
625 | }
626 |
627 | #user-info {
628 | display: flex;
629 | align-items: center;
630 | gap: 10px;
631 | }
632 |
633 | #display-name {
634 | margin-right: 5px;
635 | font-weight: 500;
636 | }
637 |
638 | .icon-button {
639 | display: flex;
640 | align-items: center;
641 | gap: 5px;
642 | background-color: rgba(255, 255, 255, 0.2);
643 | border-radius: 20px;
644 | padding: 4px 12px;
645 | font-size: 0.85rem;
646 | transition: background-color 0.2s;
647 | }
648 |
649 | .icon-button:hover {
650 | background-color: rgba(255, 255, 255, 0.3);
651 | }
652 |
653 | .user-icon {
654 | font-size: 1rem;
655 | }
656 |
657 | #logout-button {
658 | padding: 3px 10px;
659 | background-color: rgba(255, 255, 255, 0.2);
660 | font-size: 0.8rem;
661 | border-radius: 20px;
662 | }
663 |
664 | #logout-button:hover {
665 | background-color: rgba(255, 255, 255, 0.3);
666 | }
667 |
--------------------------------------------------------------------------------
/src/static/app.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", () => {
2 | // DOM elements
3 | const activitiesList = document.getElementById("activities-list");
4 | const messageDiv = document.getElementById("message");
5 | const registrationModal = document.getElementById("registration-modal");
6 | const modalActivityName = document.getElementById("modal-activity-name");
7 | const signupForm = document.getElementById("signup-form");
8 | const activityInput = document.getElementById("activity");
9 | const closeRegistrationModal = document.querySelector(".close-modal");
10 |
11 | // Search and filter elements
12 | const searchInput = document.getElementById("activity-search");
13 | const searchButton = document.getElementById("search-button");
14 | const categoryFilters = document.querySelectorAll(".category-filter");
15 | const dayFilters = document.querySelectorAll(".day-filter");
16 | const timeFilters = document.querySelectorAll(".time-filter");
17 |
18 | // Authentication elements
19 | const loginButton = document.getElementById("login-button");
20 | const userInfo = document.getElementById("user-info");
21 | const displayName = document.getElementById("display-name");
22 | const logoutButton = document.getElementById("logout-button");
23 | const loginModal = document.getElementById("login-modal");
24 | const loginForm = document.getElementById("login-form");
25 | const closeLoginModal = document.querySelector(".close-login-modal");
26 | const loginMessage = document.getElementById("login-message");
27 |
28 | // Activity categories with corresponding colors
29 | const activityTypes = {
30 | sports: { label: "Sports", color: "#e8f5e9", textColor: "#2e7d32" },
31 | arts: { label: "Arts", color: "#f3e5f5", textColor: "#7b1fa2" },
32 | academic: { label: "Academic", color: "#e3f2fd", textColor: "#1565c0" },
33 | community: { label: "Community", color: "#fff3e0", textColor: "#e65100" },
34 | technology: { label: "Technology", color: "#e8eaf6", textColor: "#3949ab" },
35 | };
36 |
37 | // State for activities and filters
38 | let allActivities = {};
39 | let currentFilter = "all";
40 | let searchQuery = "";
41 | let currentDay = "";
42 | let currentTimeRange = "";
43 |
44 | // Authentication state
45 | let currentUser = null;
46 |
47 | // Time range mappings for the dropdown
48 | const timeRanges = {
49 | morning: { start: "06:00", end: "08:00" }, // Before school hours
50 | afternoon: { start: "15:00", end: "18:00" }, // After school hours
51 | weekend: { days: ["Saturday", "Sunday"] }, // Weekend days
52 | };
53 |
54 | // Initialize filters from active elements
55 | function initializeFilters() {
56 | // Initialize day filter
57 | const activeDayFilter = document.querySelector(".day-filter.active");
58 | if (activeDayFilter) {
59 | currentDay = activeDayFilter.dataset.day;
60 | }
61 |
62 | // Initialize time filter
63 | const activeTimeFilter = document.querySelector(".time-filter.active");
64 | if (activeTimeFilter) {
65 | currentTimeRange = activeTimeFilter.dataset.time;
66 | }
67 | }
68 |
69 | // Function to set day filter
70 | function setDayFilter(day) {
71 | currentDay = day;
72 |
73 | // Update active class
74 | dayFilters.forEach((btn) => {
75 | if (btn.dataset.day === day) {
76 | btn.classList.add("active");
77 | } else {
78 | btn.classList.remove("active");
79 | }
80 | });
81 |
82 | fetchActivities();
83 | }
84 |
85 | // Function to set time range filter
86 | function setTimeRangeFilter(timeRange) {
87 | currentTimeRange = timeRange;
88 |
89 | // Update active class
90 | timeFilters.forEach((btn) => {
91 | if (btn.dataset.time === timeRange) {
92 | btn.classList.add("active");
93 | } else {
94 | btn.classList.remove("active");
95 | }
96 | });
97 |
98 | fetchActivities();
99 | }
100 |
101 | // Check if user is already logged in (from localStorage)
102 | function checkAuthentication() {
103 | const savedUser = localStorage.getItem("currentUser");
104 | if (savedUser) {
105 | try {
106 | currentUser = JSON.parse(savedUser);
107 | updateAuthUI();
108 | // Verify the stored user with the server
109 | validateUserSession(currentUser.username);
110 | } catch (error) {
111 | console.error("Error parsing saved user", error);
112 | logout(); // Clear invalid data
113 | }
114 | }
115 |
116 | // Set authentication class on body
117 | updateAuthBodyClass();
118 | }
119 |
120 | // Validate user session with the server
121 | async function validateUserSession(username) {
122 | try {
123 | const response = await fetch(
124 | `/auth/check-session?username=${encodeURIComponent(username)}`
125 | );
126 |
127 | if (!response.ok) {
128 | // Session invalid, log out
129 | logout();
130 | return;
131 | }
132 |
133 | // Session is valid, update user data
134 | const userData = await response.json();
135 | currentUser = userData;
136 | localStorage.setItem("currentUser", JSON.stringify(userData));
137 | updateAuthUI();
138 | } catch (error) {
139 | console.error("Error validating session:", error);
140 | }
141 | }
142 |
143 | // Update UI based on authentication state
144 | function updateAuthUI() {
145 | if (currentUser) {
146 | loginButton.classList.add("hidden");
147 | userInfo.classList.remove("hidden");
148 | displayName.textContent = currentUser.display_name;
149 | } else {
150 | loginButton.classList.remove("hidden");
151 | userInfo.classList.add("hidden");
152 | displayName.textContent = "";
153 | }
154 |
155 | updateAuthBodyClass();
156 | // Refresh the activities to update the UI
157 | fetchActivities();
158 | }
159 |
160 | // Update body class for CSS targeting
161 | function updateAuthBodyClass() {
162 | if (currentUser) {
163 | document.body.classList.remove("not-authenticated");
164 | } else {
165 | document.body.classList.add("not-authenticated");
166 | }
167 | }
168 |
169 | // Login function
170 | async function login(username, password) {
171 | try {
172 | const response = await fetch(
173 | `/auth/login?username=${encodeURIComponent(
174 | username
175 | )}&password=${encodeURIComponent(password)}`,
176 | {
177 | method: "POST",
178 | }
179 | );
180 |
181 | const data = await response.json();
182 |
183 | if (!response.ok) {
184 | showLoginMessage(
185 | data.detail || "Invalid username or password",
186 | "error"
187 | );
188 | return false;
189 | }
190 |
191 | // Login successful
192 | currentUser = data;
193 | localStorage.setItem("currentUser", JSON.stringify(data));
194 | updateAuthUI();
195 | closeLoginModalHandler();
196 | showMessage(`Welcome, ${currentUser.display_name}!`, "success");
197 | return true;
198 | } catch (error) {
199 | console.error("Error during login:", error);
200 | showLoginMessage("Login failed. Please try again.", "error");
201 | return false;
202 | }
203 | }
204 |
205 | // Logout function
206 | function logout() {
207 | currentUser = null;
208 | localStorage.removeItem("currentUser");
209 | updateAuthUI();
210 | showMessage("You have been logged out.", "info");
211 | }
212 |
213 | // Show message in login modal
214 | function showLoginMessage(text, type) {
215 | loginMessage.textContent = text;
216 | loginMessage.className = `message ${type}`;
217 | loginMessage.classList.remove("hidden");
218 | }
219 |
220 | // Open login modal
221 | function openLoginModal() {
222 | loginModal.classList.remove("hidden");
223 | loginModal.classList.add("show");
224 | loginMessage.classList.add("hidden");
225 | loginForm.reset();
226 | }
227 |
228 | // Close login modal
229 | function closeLoginModalHandler() {
230 | loginModal.classList.remove("show");
231 | setTimeout(() => {
232 | loginModal.classList.add("hidden");
233 | loginForm.reset();
234 | }, 300);
235 | }
236 |
237 | // Event listeners for authentication
238 | loginButton.addEventListener("click", openLoginModal);
239 | logoutButton.addEventListener("click", logout);
240 | closeLoginModal.addEventListener("click", closeLoginModalHandler);
241 |
242 | // Close login modal when clicking outside
243 | window.addEventListener("click", (event) => {
244 | if (event.target === loginModal) {
245 | closeLoginModalHandler();
246 | }
247 | });
248 |
249 | // Handle login form submission
250 | loginForm.addEventListener("submit", async (event) => {
251 | event.preventDefault();
252 | const username = document.getElementById("username").value;
253 | const password = document.getElementById("password").value;
254 | await login(username, password);
255 | });
256 |
257 | // Show loading skeletons
258 | function showLoadingSkeletons() {
259 | activitiesList.innerHTML = "";
260 |
261 | // Create more skeleton cards to fill the screen since they're smaller now
262 | for (let i = 0; i < 9; i++) {
263 | const skeletonCard = document.createElement("div");
264 | skeletonCard.className = "skeleton-card";
265 | skeletonCard.innerHTML = `
266 |
267 |
268 |
269 |
273 |
276 | `;
277 | activitiesList.appendChild(skeletonCard);
278 | }
279 | }
280 |
281 | // Format schedule for display - handles both old and new format
282 | function formatSchedule(details) {
283 | // If schedule_details is available, use the structured data
284 | if (details.schedule_details) {
285 | const days = details.schedule_details.days.join(", ");
286 |
287 | // Convert 24h time format to 12h AM/PM format for display
288 | const formatTime = (time24) => {
289 | const [hours, minutes] = time24.split(":").map((num) => parseInt(num));
290 | const period = hours >= 12 ? "PM" : "AM";
291 | const displayHours = hours % 12 || 12; // Convert 0 to 12 for 12 AM
292 | return `${displayHours}:${minutes
293 | .toString()
294 | .padStart(2, "0")} ${period}`;
295 | };
296 |
297 | const startTime = formatTime(details.schedule_details.start_time);
298 | const endTime = formatTime(details.schedule_details.end_time);
299 |
300 | return `${days}, ${startTime} - ${endTime}`;
301 | }
302 |
303 | // Fallback to the string format if schedule_details isn't available
304 | return details.schedule;
305 | }
306 |
307 | // Function to determine activity type (this would ideally come from backend)
308 | function getActivityType(activityName, description) {
309 | const name = activityName.toLowerCase();
310 | const desc = description.toLowerCase();
311 |
312 | if (
313 | name.includes("soccer") ||
314 | name.includes("basketball") ||
315 | name.includes("sport") ||
316 | name.includes("fitness") ||
317 | desc.includes("team") ||
318 | desc.includes("game") ||
319 | desc.includes("athletic")
320 | ) {
321 | return "sports";
322 | } else if (
323 | name.includes("art") ||
324 | name.includes("music") ||
325 | name.includes("theater") ||
326 | name.includes("drama") ||
327 | desc.includes("creative") ||
328 | desc.includes("paint")
329 | ) {
330 | return "arts";
331 | } else if (
332 | name.includes("science") ||
333 | name.includes("math") ||
334 | name.includes("academic") ||
335 | name.includes("study") ||
336 | name.includes("olympiad") ||
337 | desc.includes("learning") ||
338 | desc.includes("education") ||
339 | desc.includes("competition")
340 | ) {
341 | return "academic";
342 | } else if (
343 | name.includes("volunteer") ||
344 | name.includes("community") ||
345 | desc.includes("service") ||
346 | desc.includes("volunteer")
347 | ) {
348 | return "community";
349 | } else if (
350 | name.includes("computer") ||
351 | name.includes("coding") ||
352 | name.includes("tech") ||
353 | name.includes("robotics") ||
354 | desc.includes("programming") ||
355 | desc.includes("technology") ||
356 | desc.includes("digital") ||
357 | desc.includes("robot")
358 | ) {
359 | return "technology";
360 | }
361 |
362 | // Default to "academic" if no match
363 | return "academic";
364 | }
365 |
366 | // Function to fetch activities from API with optional day and time filters
367 | async function fetchActivities() {
368 | // Show loading skeletons first
369 | showLoadingSkeletons();
370 |
371 | try {
372 | // Build query string with filters if they exist
373 | let queryParams = [];
374 |
375 | // Handle day filter
376 | if (currentDay) {
377 | queryParams.push(`day=${encodeURIComponent(currentDay)}`);
378 | }
379 |
380 | // Handle time range filter
381 | if (currentTimeRange) {
382 | const range = timeRanges[currentTimeRange];
383 |
384 | // Handle weekend special case
385 | if (currentTimeRange === "weekend") {
386 | // Don't add time parameters for weekend filter
387 | // Weekend filtering will be handled on the client side
388 | } else if (range) {
389 | // Add time parameters for before/after school
390 | queryParams.push(`start_time=${encodeURIComponent(range.start)}`);
391 | queryParams.push(`end_time=${encodeURIComponent(range.end)}`);
392 | }
393 | }
394 |
395 | const queryString =
396 | queryParams.length > 0 ? `?${queryParams.join("&")}` : "";
397 | const response = await fetch(`/activities${queryString}`);
398 | const activities = await response.json();
399 |
400 | // Save the activities data
401 | allActivities = activities;
402 |
403 | // Apply search and filter, and handle weekend filter in client
404 | displayFilteredActivities();
405 | } catch (error) {
406 | activitiesList.innerHTML =
407 | "Failed to load activities. Please try again later.
";
408 | console.error("Error fetching activities:", error);
409 | }
410 | }
411 |
412 | // Function to display filtered activities
413 | function displayFilteredActivities() {
414 | // Clear the activities list
415 | activitiesList.innerHTML = "";
416 |
417 | // Apply client-side filtering - this handles category filter and search, plus weekend filter
418 | let filteredActivities = {};
419 |
420 | Object.entries(allActivities).forEach(([name, details]) => {
421 | const activityType = getActivityType(name, details.description);
422 |
423 | // Apply category filter
424 | if (currentFilter !== "all" && activityType !== currentFilter) {
425 | return;
426 | }
427 |
428 | // Apply weekend filter if selected
429 | if (currentTimeRange === "weekend" && details.schedule_details) {
430 | const activityDays = details.schedule_details.days;
431 | const isWeekendActivity = activityDays.some((day) =>
432 | timeRanges.weekend.days.includes(day)
433 | );
434 |
435 | if (!isWeekendActivity) {
436 | return;
437 | }
438 | }
439 |
440 | // Apply search filter
441 | const searchableContent = [
442 | name.toLowerCase(),
443 | details.description.toLowerCase(),
444 | formatSchedule(details).toLowerCase(),
445 | ].join(" ");
446 |
447 | if (
448 | searchQuery &&
449 | !searchableContent.includes(searchQuery.toLowerCase())
450 | ) {
451 | return;
452 | }
453 |
454 | // Activity passed all filters, add to filtered list
455 | filteredActivities[name] = details;
456 | });
457 |
458 | // Check if there are any results
459 | if (Object.keys(filteredActivities).length === 0) {
460 | activitiesList.innerHTML = `
461 |
462 |
No activities found
463 |
Try adjusting your search or filter criteria
464 |
465 | `;
466 | return;
467 | }
468 |
469 | // Display filtered activities
470 | Object.entries(filteredActivities).forEach(([name, details]) => {
471 | renderActivityCard(name, details);
472 | });
473 | }
474 |
475 | // Function to render a single activity card
476 | function renderActivityCard(name, details) {
477 | const activityCard = document.createElement("div");
478 | activityCard.className = "activity-card";
479 |
480 | // Calculate spots and capacity
481 | const totalSpots = details.max_participants;
482 | const takenSpots = details.participants.length;
483 | const spotsLeft = totalSpots - takenSpots;
484 | const capacityPercentage = (takenSpots / totalSpots) * 100;
485 | const isFull = spotsLeft <= 0;
486 |
487 | // Determine capacity status class
488 | let capacityStatusClass = "capacity-available";
489 | if (isFull) {
490 | capacityStatusClass = "capacity-full";
491 | } else if (capacityPercentage >= 75) {
492 | capacityStatusClass = "capacity-near-full";
493 | }
494 |
495 | // Determine activity type
496 | const activityType = getActivityType(name, details.description);
497 | const typeInfo = activityTypes[activityType];
498 |
499 | // Format the schedule using the new helper function
500 | const formattedSchedule = formatSchedule(details);
501 |
502 | // Create activity tag
503 | const tagHtml = `
504 |
505 | ${typeInfo.label}
506 |
507 | `;
508 |
509 | // Create capacity indicator
510 | const capacityIndicator = `
511 |
512 |
515 |
516 | ${takenSpots} enrolled
517 | ${spotsLeft} spots left
518 |
519 |
520 | `;
521 |
522 | activityCard.innerHTML = `
523 | ${tagHtml}
524 | ${name}
525 | ${details.description}
526 |
527 | Schedule: ${formattedSchedule}
528 | Regular meetings at this time throughout the semester
529 |
530 | ${capacityIndicator}
531 |
532 |
Current Participants:
533 |
534 | ${details.participants
535 | .map(
536 | (email) => `
537 | -
538 | ${email}
539 | ${
540 | currentUser
541 | ? `
542 |
543 | ✖
544 | Unregister this student
545 |
546 | `
547 | : ""
548 | }
549 |
550 | `
551 | )
552 | .join("")}
553 |
554 |
555 |
556 | ${
557 | currentUser
558 | ? `
559 |
564 | `
565 | : `
566 |
567 | Teachers can register students.
568 |
569 | `
570 | }
571 |
572 | `;
573 |
574 | // Add click handlers for delete buttons
575 | const deleteButtons = activityCard.querySelectorAll(".delete-participant");
576 | deleteButtons.forEach((button) => {
577 | button.addEventListener("click", handleUnregister);
578 | });
579 |
580 | // Add click handler for register button (only when authenticated)
581 | if (currentUser) {
582 | const registerButton = activityCard.querySelector(".register-button");
583 | if (!isFull) {
584 | registerButton.addEventListener("click", () => {
585 | openRegistrationModal(name);
586 | });
587 | }
588 | }
589 |
590 | activitiesList.appendChild(activityCard);
591 | }
592 |
593 | // Event listeners for search and filter
594 | searchInput.addEventListener("input", (event) => {
595 | searchQuery = event.target.value;
596 | displayFilteredActivities();
597 | });
598 |
599 | searchButton.addEventListener("click", (event) => {
600 | event.preventDefault();
601 | searchQuery = searchInput.value;
602 | displayFilteredActivities();
603 | });
604 |
605 | // Add event listeners to category filter buttons
606 | categoryFilters.forEach((button) => {
607 | button.addEventListener("click", () => {
608 | // Update active class
609 | categoryFilters.forEach((btn) => btn.classList.remove("active"));
610 | button.classList.add("active");
611 |
612 | // Update current filter and display filtered activities
613 | currentFilter = button.dataset.category;
614 | displayFilteredActivities();
615 | });
616 | });
617 |
618 | // Add event listeners to day filter buttons
619 | dayFilters.forEach((button) => {
620 | button.addEventListener("click", () => {
621 | // Update active class
622 | dayFilters.forEach((btn) => btn.classList.remove("active"));
623 | button.classList.add("active");
624 |
625 | // Update current day filter and fetch activities
626 | currentDay = button.dataset.day;
627 | fetchActivities();
628 | });
629 | });
630 |
631 | // Add event listeners for time filter buttons
632 | timeFilters.forEach((button) => {
633 | button.addEventListener("click", () => {
634 | // Update active class
635 | timeFilters.forEach((btn) => btn.classList.remove("active"));
636 | button.classList.add("active");
637 |
638 | // Update current time filter and fetch activities
639 | currentTimeRange = button.dataset.time;
640 | fetchActivities();
641 | });
642 | });
643 |
644 | // Open registration modal
645 | function openRegistrationModal(activityName) {
646 | modalActivityName.textContent = activityName;
647 | activityInput.value = activityName;
648 | registrationModal.classList.remove("hidden");
649 | // Add slight delay to trigger animation
650 | setTimeout(() => {
651 | registrationModal.classList.add("show");
652 | }, 10);
653 | }
654 |
655 | // Close registration modal
656 | function closeRegistrationModalHandler() {
657 | registrationModal.classList.remove("show");
658 | setTimeout(() => {
659 | registrationModal.classList.add("hidden");
660 | signupForm.reset();
661 | }, 300);
662 | }
663 |
664 | // Event listener for close button
665 | closeRegistrationModal.addEventListener(
666 | "click",
667 | closeRegistrationModalHandler
668 | );
669 |
670 | // Close modal when clicking outside of it
671 | window.addEventListener("click", (event) => {
672 | if (event.target === registrationModal) {
673 | closeRegistrationModalHandler();
674 | }
675 | });
676 |
677 | // Create and show confirmation dialog
678 | function showConfirmationDialog(message, confirmCallback) {
679 | // Create the confirmation dialog if it doesn't exist
680 | let confirmDialog = document.getElementById("confirm-dialog");
681 | if (!confirmDialog) {
682 | confirmDialog = document.createElement("div");
683 | confirmDialog.id = "confirm-dialog";
684 | confirmDialog.className = "modal hidden";
685 | confirmDialog.innerHTML = `
686 |
687 |
Confirm Action
688 |
689 |
690 |
691 |
692 |
693 |
694 | `;
695 | document.body.appendChild(confirmDialog);
696 |
697 | // Style the buttons
698 | const cancelBtn = confirmDialog.querySelector("#cancel-button");
699 | const confirmBtn = confirmDialog.querySelector("#confirm-button");
700 |
701 | cancelBtn.style.backgroundColor = "#f1f1f1";
702 | cancelBtn.style.color = "#333";
703 |
704 | confirmBtn.style.backgroundColor = "#dc3545";
705 | confirmBtn.style.color = "white";
706 | }
707 |
708 | // Set the message
709 | const confirmMessage = document.getElementById("confirm-message");
710 | confirmMessage.textContent = message;
711 |
712 | // Show the dialog
713 | confirmDialog.classList.remove("hidden");
714 | setTimeout(() => {
715 | confirmDialog.classList.add("show");
716 | }, 10);
717 |
718 | // Handle button clicks
719 | const cancelButton = document.getElementById("cancel-button");
720 | const confirmButton = document.getElementById("confirm-button");
721 |
722 | // Remove any existing event listeners
723 | const newCancelButton = cancelButton.cloneNode(true);
724 | const newConfirmButton = confirmButton.cloneNode(true);
725 | cancelButton.parentNode.replaceChild(newCancelButton, cancelButton);
726 | confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
727 |
728 | // Add new event listeners
729 | newCancelButton.addEventListener("click", () => {
730 | confirmDialog.classList.remove("show");
731 | setTimeout(() => {
732 | confirmDialog.classList.add("hidden");
733 | }, 300);
734 | });
735 |
736 | newConfirmButton.addEventListener("click", () => {
737 | confirmCallback();
738 | confirmDialog.classList.remove("show");
739 | setTimeout(() => {
740 | confirmDialog.classList.add("hidden");
741 | }, 300);
742 | });
743 |
744 | // Close when clicking outside
745 | confirmDialog.addEventListener("click", (event) => {
746 | if (event.target === confirmDialog) {
747 | confirmDialog.classList.remove("show");
748 | setTimeout(() => {
749 | confirmDialog.classList.add("hidden");
750 | }, 300);
751 | }
752 | });
753 | }
754 |
755 | // Handle unregistration with confirmation
756 | async function handleUnregister(event) {
757 | // Check if user is authenticated
758 | if (!currentUser) {
759 | showMessage(
760 | "You must be logged in as a teacher to unregister students.",
761 | "error"
762 | );
763 | return;
764 | }
765 |
766 | const activity = event.target.dataset.activity;
767 | const email = event.target.dataset.email;
768 |
769 | // Show confirmation dialog
770 | showConfirmationDialog(
771 | `Are you sure you want to unregister ${email} from ${activity}?`,
772 | async () => {
773 | try {
774 | const response = await fetch(
775 | `/activities/${encodeURIComponent(
776 | activity
777 | )}/unregister?email=${encodeURIComponent(
778 | email
779 | )}&teacher_username=${encodeURIComponent(currentUser.username)}`,
780 | {
781 | method: "POST",
782 | }
783 | );
784 |
785 | const result = await response.json();
786 |
787 | if (response.ok) {
788 | showMessage(result.message, "success");
789 | // Refresh the activities list
790 | fetchActivities();
791 | } else {
792 | showMessage(result.detail || "An error occurred", "error");
793 | }
794 | } catch (error) {
795 | showMessage("Failed to unregister. Please try again.", "error");
796 | console.error("Error unregistering:", error);
797 | }
798 | }
799 | );
800 | }
801 |
802 | // Show message function
803 | function showMessage(text, type) {
804 | messageDiv.textContent = text;
805 | messageDiv.className = `message ${type}`;
806 | messageDiv.classList.remove("hidden");
807 |
808 | // Hide message after 5 seconds
809 | setTimeout(() => {
810 | messageDiv.classList.add("hidden");
811 | }, 5000);
812 | }
813 |
814 | // Handle form submission
815 | signupForm.addEventListener("submit", async (event) => {
816 | event.preventDefault();
817 |
818 | // Check if user is authenticated
819 | if (!currentUser) {
820 | showMessage(
821 | "You must be logged in as a teacher to register students.",
822 | "error"
823 | );
824 | return;
825 | }
826 |
827 | const email = document.getElementById("email").value;
828 | const activity = activityInput.value;
829 |
830 | try {
831 | const response = await fetch(
832 | `/activities/${encodeURIComponent(
833 | activity
834 | )}/signup?email=${encodeURIComponent(
835 | email
836 | )}&teacher_username=${encodeURIComponent(currentUser.username)}`,
837 | {
838 | method: "POST",
839 | }
840 | );
841 |
842 | const result = await response.json();
843 |
844 | if (response.ok) {
845 | showMessage(result.message, "success");
846 | closeRegistrationModalHandler();
847 | // Refresh the activities list after successful signup
848 | fetchActivities();
849 | } else {
850 | showMessage(result.detail || "An error occurred", "error");
851 | }
852 | } catch (error) {
853 | showMessage("Failed to sign up. Please try again.", "error");
854 | console.error("Error signing up:", error);
855 | }
856 | });
857 |
858 | // Expose filter functions to window for future UI control
859 | window.activityFilters = {
860 | setDayFilter,
861 | setTimeRangeFilter,
862 | };
863 |
864 | // Initialize app
865 | checkAuthentication();
866 | initializeFilters();
867 | fetchActivities();
868 | });
869 |
--------------------------------------------------------------------------------