├── 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 | celebrate 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://img.shields.io/badge/Copy%20Exercise-%E2%86%92-1f883d?style=for-the-badge&logo=github&labelColor=197935)](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 | image showing the correct branch 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 | 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 | 145 | 146 | 147 | 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 | image 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 | image
113 | 114 | image
115 | 116 | image 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 | image 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 | image 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 | image 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 | image 40 | 41 | 1. Set the `package-ecosytem` to `pip` so Dependabot will automatically monitor our Python requirements. 42 | 43 | image 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 | image 61 | 62 | 1. Click the **Enable CodeQL** button to accept the default configuration. 63 | 64 | image 65 | 66 | 1. Below the **Tools** section. Verify **Copilot Autofix** is set to `On`. 67 | 68 | image 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 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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 | copilot extension for VS Code
43 | python extension for VS Code 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 | run and debug 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 | empty run and debug panel 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 | ports tab 69 | 70 | ![Screenshot of Mergington High School WebApp](https://github.com/user-attachments/assets/5e1e7c1e-1b0e-4378-a5af-a266763e6544) 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 | image 87 | 88 | 1. Set the **Ruleset Name** as `Protect main` and change the **Enforcement status** to `Active`. 89 | 90 | image 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 | image 97 | 98 | 1. Use the **include by pattern** option and enter the pattern `main`. 99 | 100 | image 101 | 102 | image 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 | New file button 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 | preview of new file 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 | image 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 |
270 |
271 |
272 |
273 |
274 |
275 |
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 |
513 |
514 |
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 | 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 | 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 | --------------------------------------------------------------------------------