Initial commit
Diff
LICENSE | 21 +++++++++++++++++++++
README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
requirements.txt | 4 ++++
.devcontainer/devcontainer.json | 16 ++++++++++++++++
.devcontainer/installMongoDB.sh | 21 +++++++++++++++++++++
.devcontainer/postCreate.sh | 5 +++++
.vscode/launch.json | 19 +++++++++++++++++++
src/README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
src/app.py | 35 +++++++++++++++++++++++++++++++++++
.github/steps/1-protect-your-code.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/steps/2-prepare-to-collaborate.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/steps/3-foster-healthy-growth.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/steps/4-prepare-for-the-inevitable.md | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/steps/5-merge.md | 13 +++++++++++++
.github/steps/x-review.md | 36 ++++++++++++++++++++++++++++++++++++
.github/workflows/0-start-exercise.yml | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/workflows/1-protect-your-code.yml | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/workflows/2-prepare-to-collaborate.yml | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/workflows/3-foster-healthy-growth.yml | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/workflows/4-prepare-for-the-inevitable.yml | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.github/workflows/5-merge.yml | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/__init__.py | 2 ++
src/backend/database.py | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/static/app.js | 868 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/static/index.html | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/static/styles.css | 666 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/routers/__init__.py | 2 ++
src/backend/routers/activities.py | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/routers/auth.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
29 files changed, 3845 insertions(+)
@@ -1,0 +1,21 @@
MIT License
Copyright (c) 2025 GitHub Skills
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,0 +1,54 @@
# Introduction to Repository Management
_Learn the basics of several GitHub features that can help support a collaborative, friendly, and healthy project._
## Welcome
- **Who is this for**: Developers with the need to start collaborating.
- **What you'll learn**: The different ways to protect your repository's content as more people join as collaborators.
- **What you'll build**: You will prepare Mergington High School's extracurricular activities website repository so additional teachers can safely collaborate.
- **Prerequisites**:
- Skills exercise: [Introduction to GitHub](https://github.com/skills/introduction-to-github)
- Skills exercise: [Communicate using Markdown](https://github.com/skills/communicate-using-markdown)
- Skills exercise: [Review pull requests](https://github.com/skills/review-pull-requests)
- **How long**: This exercise takes less than one hour to complete.
In this exercise, you will:
1. Add a simple rulesets and configuration to restrict repository content.
1. Communicate procedures to help guide collaborators.
1. Assign responsibility of parts of the code to particular collaborators.
1. Learn the difference between collaboration in a personal repository and organization repository.
1. Establish ground rules to promote a health collaboration environment.
1. Establish a process for managing security updates.
> [!IMPORTANT]
> This exercise is meant to provide an overview of many GitHub features.
> It will provide references to learn more but not a detailed explanation for any specific subject.
### How to start this exercise
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**.
[](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)
<details>
<summary>Having trouble? 🤷</summary><br/>
When copying the exercise, we recommend the following settings:
- For owner, choose your personal account or an organization to host the repository.
- We recommend creating a public repository, since private repositories will use Actions minutes.
If the exercise isn't ready in 20 seconds, please check the [Actions](../../actions) tab.
- Check to see if a job is running. Sometimes it simply takes a bit longer.
- If the page shows a failed job, please submit an issue. Nice, you found a bug! 🐛
</details>
---
© 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)
@@ -1,0 +1,4 @@
fastapi
uvicorn
pymongo
argon2-cffi==23.1.0
@@ -1,0 +1,16 @@
{
"name": "Python 3",
"image": "mcr.microsoft.com/vscode/devcontainers/python:3.13",
"forwardPorts": [8000],
"postCreateCommand": "bash ./.devcontainer/postCreate.sh",
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot",
"ms-python.python",
"ms-python.debugpy",
"mongodb.mongodb-vscode"
]
}
}
}
@@ -1,0 +1,21 @@
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo mkdir -p /data/db
sudo chown -R mongodb:mongodb /data/db
sudo mongod --fork --logpath /var/log/mongodb/mongod.log
echo "MongoDB has been installed and started successfully!"
mongod --version
echo "Current databases:"
mongosh --eval "db.getMongo().getDBNames()"
@@ -1,0 +1,5 @@
pip install -r requirements.txt
./.devcontainer/installMongoDB.sh
@@ -1,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Mergington WebApp",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"src.app:app",
"--reload"
],
"jinja": true
}
]
}
@@ -1,0 +1,50 @@
# Mergington High School Activities API
A super simple FastAPI application that allows students to view and sign up for extracurricular activities.
## Features
- View all available extracurricular activities
- Sign up for activities
## Getting Started
1. Install the dependencies:
```
pip install fastapi uvicorn
```
2. Run the application:
```
python app.py
```
3. Open your browser and go to:
- API documentation: http://localhost:8000/docs
- Alternative documentation: http://localhost:8000/redoc
## API Endpoints
| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| GET | `/activities` | Get all activities with their details and current participant count |
| POST | `/activities/{activity_name}/signup?email=student@mergington.edu` | Sign up for an activity |
## Data Model
The application uses a simple data model with meaningful identifiers:
1. **Activities** - Uses activity name as identifier:
- Description
- Schedule
- Maximum number of participants allowed
- List of student emails who are signed up
2. **Students** - Uses email as identifier:
- Name
- Grade level
All data is stored in memory, which means data will be reset when the server restarts.
@@ -1,0 +1,35 @@
"""
High School Management System API
A super simple FastAPI application that allows students to view and sign up
for extracurricular activities at Mergington High School.
"""
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
import os
from pathlib import Path
from .backend import routers, database
app = FastAPI(
title="Mergington High School API",
description="API for viewing and signing up for extracurricular activities"
)
database.init_database()
current_dir = Path(__file__).parent
app.mount("/static", StaticFiles(directory=os.path.join(current_dir, "static")), name="static")
@app.get("/")
def root():
return RedirectResponse(url="/static/index.html")
app.include_router(routers.activities.router)
app.include_router(routers.auth.router)
@@ -1,0 +1,173 @@
# Step 1: Protect your code
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. 📚✨
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! 😰
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:
1. **Repository Rulesets** - These provide safeguards to limit:
- Pushing code directly to important branches
- Deleting or renaming branches
- Force pushing (which can overwrite history)
- (and much more)
1. **`.gitignore`** - This special file tells Git which files it should NOT track, like:
- Temporary files that your code creates while running
- Secret configuration files with sensitive information
- System files that other developers don't need
> [!TIP]
> 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.
## ⌨️ Activity: (optional) Get to know our extracurricular activities site
<details>
<summary>Show Steps</summary>
In other exercise, we have been developing the Extracurricular activities website. You can follow these steps to start up the development environment and try it out.
> ! **Important:** Opening a development environment and running the application is not necessary to complete this exercise. You can skip this activity if desired.
1. Right-click the below button to open the **Create Codespace** page in a new tab. Use the default configuration.
[](https://codespaces.new/{{full_repo_name}}?quickstart=1)
1. Wait some time for the environment to be prepared. It will automatically install all requirements and services.
1. Validate the **GitHub Copilot** and **Python** extensions are installed and enabled.
<img width="300" alt="copilot extension for VS Code" src="https://github.com/user-attachments/assets/ef1ef984-17fc-4b20-a9a6-65a866def468" /><br/>
<img width="300" alt="python extension for VS Code" src="https://github.com/user-attachments/assets/3040c0f5-1658-47e2-a439-20504a384f77" />
1. Try running the the application. In the left sidebar, select the **Run and Debug** tab and then press the **Start Debugging** icon.
<details>
<summary>📸 Show screenshot</summary><br/>
<img width="300" alt="run and debug" src="https://github.com/user-attachments/assets/50b27f2a-5eab-4827-9343-ab5bce62357e" />
</details>
<details>
<summary>🤷 Having trouble?</summary><br/>
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`.
<img width="300" alt="empty run and debug panel" src="https://github.com/user-attachments/assets/0dbf1407-3a97-401a-a630-f462697082d6" />
</details>
1. Use the **Ports** tab to find the webpage address, open it, and verify it is running.
<details>
<summary>📸 Show screenshot</summary><br/>
<img width="350" alt="ports tab" src="https://github.com/user-attachments/assets/8d24d6b5-202d-4109-8174-2f0d1e4d8d44" />

</details>
</details>
## ⌨️ Activity: Add a branch ruleset
To get started, let's add some protections so that no one accidentally breaks the club registration system.
1. If necessary, open another tab and navigate to this repository. We will start on the **Settings** tab.
1. In the left navigation, expand the **Rules** area and select **Rulesets**.
1. Click the **New ruleset** dropdown and select **New branch ruleset**.
<img width="250" alt="image" src="https://github.com/user-attachments/assets/1e9fd519-1421-4d6b-b654-a3fe53a8fb75" />
1. Set the **Ruleset Name** as `Protect main` and change the **Enforcement status** to `Active`.
<img width="250" alt="image" src="https://github.com/user-attachments/assets/ce30fd34-39b5-4e22-b348-4af61fd05cd1" />
1. Find the the **Targets** section and use the **Add target** dropdown to add 2 entries:
1. Add the **Include default branch** option to ensure protections aren't bypassed by switching the default branch.
<img width="250" alt="image" src="https://github.com/user-attachments/assets/217263cc-d5c2-4ac0-b03c-a72494e5c812" />
1. Use the **include by pattern** option and enter the pattern `main`.
<img width="250" alt="image" src="https://github.com/user-attachments/assets/968c9ed8-b051-44eb-af42-d99670ad31fd" />
<img width="250" alt="image" src="https://github.com/user-attachments/assets/ddc52767-d93e-4c9e-a77a-90c3b5c08fb5" />
1. Find the **Rules** section and ensure the following items are checked.
- [x] Restrict deletions
- [x] Require a pull request before merging
- Required approvals: `0`
- [x] Require review from Code Owners
- [x] Block force pushes
1. Scroll to the bottom and click the **Create** button to save the ruleset.
## ⌨️ Activity: Create a `.gitignore` file
We know many teachers use different tools, so let's make sure they don't accidentally commit unnecessary files.
1. At the top navigation, return to the **Code** tab ane verify you are on the `main` branch.
1. Above the list of files, click the **Add file** dropdown and select **Create new file**.
<img width="300" alt="New file button" src="https://github.com/user-attachments/assets/8f3f8da8-1471-485a-9df5-8c03ecba2d8e"/>
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.
<img width="350" alt="preview of new file" src="https://github.com/user-attachments/assets/580d1a63-a264-4d44-8901-50ad708b8822"/>
```gitignore
# Python backend for club management
__pycache__/
*.py[cod] # Python compiled files
*$py.class
*.so
.Python
env/
.env # Where database passwords are stored
venv/ # Virtual environment for testing
.venv
# Teacher IDE settings
.vscode/ # Ms. Rodriguez uses VS Code
.idea/ # Mr. Chen uses PyCharm
# Local development & testing
instance/
.pytest_cache/
.coverage # Test coverage reports
htmlcov/
# Staff computer files
.DS_Store # For teachers with Macs
Thumbs.db # For teachers with Windows
```
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!
<img width="400" alt="image" src="https://github.com/user-attachments/assets/4e85948d-75c8-4c13-8ddd-4707bf9b0805" />
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.
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.
1. With the file committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
> [!TIP]
> 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!
<details>
<summary>🤷 Having trouble?</summary><br/>
Make sure you pushed the `.gitignore` file to `prepare-to-collaborate` branch. Exact naming for both matters!
</details>
@@ -1,0 +1,134 @@
# Step 2: Prepare to collaborate
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!
- Ms. Rodriguez wants to add a photo gallery
- Mr. Chen dreams of adding a tournament bracket system for the chess/sports activities! 🎨♟️
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!
Opening your project to other teachers at Mergington High means thinking about how everyone will collaborate together without breaking each other's code.
**Collaborators** are the people you have granted write access to the project through repository settings.
- Provide other people permissions to change project files while still protecting repository settings.
- Personal repositories have simple permissions. Organization repositories allow flexible permissions such as read, write, maintain, and admin.
To help with collaboration, GitHub provides two special files:
1. **`CONTRIBUTING.md` file** - A "How to Help" guide. Some example content:
- How to prepare a developer setup of the extra-curricular activities website.
- The process for suggesting changes.
- The project's coding style preference, to keep things consistent.
- How to ask for help when stuck.
1. **`CODEOWNERS` file** - Assign specific people or teams responsible for a portion of the project.
- When someone creates a pull request, GitHub will automatically ask the right person to review it.
## ⌨️ Activity: Create a welcoming contribution guide
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.
1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch.
<img width="265" alt="image showing the correct branch" src="https://github.com/user-attachments/assets/bd12d0cc-0920-4158-9e96-e3a8fb994c1a" />
1. In the top directory, create a new file called `CONTRIBUTING.md` (case sensitive).
1. Add a welcoming message.
```md
# Contributing to the Mergington High Extra-Curricular Activities Website
Thank you for your interest in helping improve our school's website!
Whether you want to add your club's activities, fix a bug, or suggest
new features, this guide will help you get started. 🎉
```
1. Add instructions to help teachers quickly start developing.
```md
## Development Setup
1. Clone the repository to your computer.
2. Install Python requirements: `pip install -r requirements.txt`.
3. Run the development server: `python src/app.py`.
4. Visit http://localhost:8000 in your browser to see the website.
## Making Changes
1. Create a new branch for your changes.
- Use descriptive names like `art-gallery-feature` or `fix-chess-signup`
2. Make your changes and test them locally with sample student data.
- Use the MongoDB extension to preview the included sample date.
3. Push your branch and create a pull request.
4. Wait for review and address any feedback.
## Code Style
- Follow PEP 8 for Python code (backend).
- Use clear, descriptive variable names (student_name, start_time, etc.)
- Add comments to describe blocks of logic.
```
1. Add a section for getting help.
```md
## Need help or have ideas?
- Check the open issues first.
- If your problem is there, add a comment or up-vote.
- If not there, create a new issue. Be as descriptive as possible.
- Ask in our weekly IT Club office hours (Thursdays at lunch in Room 203).
- For other general problems, email the tech team at techclub@mergingtonhigh.example.edu
```
1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
## ⌨️ Activity: Assign code ownership
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.
1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch.
1. In the top directory, create a new file called `CODEOWNERS` (case sensitive and no extension).
1. Add the following content:
```codeowners
# Core functionality - changes here should be rare!
/src/app.py @{{ login }}
/src/backend/database.py @{{ login }}
/src/backend/routers/auth.py @{{ login }}
# The frontend will need refactored soon to be more object oriented.
/src/static/ @{{ login }}
```
1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
## ⌨️ Activity: (Optional) Add your first collaborator
Ready to let your colleague start working on that photo gallery feature? Let's do it!
> [!IMPORTANT]
> This step is optional because it requires another person with a GitHub account to participate.
1. In the top navigation, select the **Settings** tab.
1. In the left navigation, select **Collaborators**.
1. Find the **Manage access** area and click the **Add people** button.
<img width="350" alt="" src="https://github.com/user-attachments/assets/686c32c6-11c2-4e69-bad1-39062d5b4376" />
1. Enter a friend/colleague's GitHub username or email then press the **Add to repository** button.
<img width="350" alt="" src="https://github.com/user-attachments/assets/d0eaf344-baf0-4a9c-9291-c11e7a9fdaa3" />
> [!IMPORTANT]
> 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).
@@ -1,0 +1,131 @@
# Step 3: Foster healthy growth
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?"
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.
Let's look at a couple ways to do that:
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.
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.
## ⌨️ Activity: Set expectations with a Code of Conduct
Let's start by establishing some community guidelines for your growing team of teacher-contributors.
> [!TIP]
> The [Contributor Covenant](https://www.contributor-covenant.org/) is a popular code of conduct used by many projects.
1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch.
1. In the top directory, create a new file called `CODE_OF_CONDUCT.md` (case sensitive).
1. Add the following content:
```markdown
# Mergington High School Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment for
our school community, we as contributors and maintainers pledge to
make participation in the Extra-Curricular Activities project a
respectful and harassment-free experience for everyone.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the students and the school community
- Showing empathy towards other community members
Examples of unacceptable behavior include:
- The use of inappropriate language or imagery
- Trolling, insulting comments, and personal attacks
- Public or private harassment
- Publishing others' private information without explicit permission
- Other conduct which could reasonably be considered inappropriate in a school setting
## Responsibilities
Project maintainers are responsible for clarifying the standards of
acceptable behavior and are expected to take appropriate and fair
corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit,
or reject comments, commits, code, issues, and other contributions that
are not aligned to this Code of Conduct.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or the school. Examples of
representing the project include using an official project email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the IT Club faculty advisor. All complaints will be reviewed and
investigated promptly and fairly.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may
face temporary or permanent repercussions as determined by the school administration.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
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)
```
1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
## ⌨️ Activity: Communicate easier with issue templates
Now let's create templates so other teachers can report bugs or request features in a standardized way.
> [!TIP]
> 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.
1. In the top navigation, select the **Settings** tab.
1. Find the **Features** section and verify **Issues** is enabled.
<img width="350" alt="" src="https://github.com/user-attachments/assets/dafb976b-4b8c-4c5e-8989-04d3e7bbe70d" />
1. Click the **Set up templates** button to enter the issue templates editor.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/bd94af1e-d564-472f-a435-f12fa1bf3b5c" />
1. Click the **Add template** dropdown and select **Bug report**.
<img width="350" alt="" src="https://github.com/user-attachments/assets/baee263d-b233-4029-b629-9544eacf1e27" />
1. Click the **Preview and edit** button to show the current template. Click the **Edit icon** (pencil) to make the fields editable.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/1c8500f7-10b2-406b-9385-d5b9480e2f71" /><br/>
<img width="350" alt="image" src="https://github.com/user-attachments/assets/77e312e2-af3c-4015-94f0-b1cf7409cc40" /><br/>
<img width="700" alt="image" src="https://github.com/user-attachments/assets/c2aecd6e-d021-4149-b088-7cbf883a7e33" />
1. (Optional) Let's keep it simple for our students and fellow teachers. Remove the sections about Desktop and Smartphone details.
1. Repeat the above steps for the "Feature request" template.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/6456e261-fcd8-4845-b1ab-f2c2d5883c77" />
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.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/a00a3740-ce0c-430c-9541-e56b7d9b45d6" />
1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
> [!TIP]
> Did you notice that you are working in parallel on 2 branches now? That's exactly what working with multiple collaborators is like.
@@ -1,0 +1,130 @@
# Step 4: Prepare for the inevitable
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. 😱
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:
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.
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.
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.
> [!NOTE]
> 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.
## ⌨️ Activity: Automate security updates with Dependabot
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!
> [!TIP]
> For a deeper dive, check out the [Secure Repository Supply Chain](https://github.com/skills/secure-repository-supply-chain) Skills exercise!
1. In the top navigation, select the **Settings** tab.
1. In the left navigation, select **Advanced Security**.
1. Find the **Dependabot** section. Verify or change the settings to match the following:
- **Dependabot alerts**: `enabled`
- **Dependabot security updates**: `enabled`
- **Grouped security updates**: `enabled`
- **Dependabot on Actions runners**: `enabled`
1. Find **Dependabot version updates** and click the **Enable** button. This will open an editor to create a configuration file.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/a4d7ae19-0439-4b78-bcbf-9fce5c5410ff" />
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`.
<img width="500" alt="image" src="https://github.com/user-attachments/assets/18a3cd1a-75ab-4e5e-a4c4-efd175d91ced" />
1. Set the `package-ecosytem` to `pip` so Dependabot will automatically monitor our Python requirements.
<img width="500" alt="image" src="https://github.com/user-attachments/assets/0bc90e67-4b71-4780-8272-20dc0fff5c4c" />
1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
## ⌨️ Activity: Detect dangerous patterns with code scanning
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.
> [!TIP]
> 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!
1. In the top navigation, select the **Settings** tab.
1. In the left navigation, select **Advanced Security**.
1. Find the **Code scanning** section. Click the **Set up** button and select the **Default** option to open a configuration panel.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/5b3a89e5-c71a-44d9-b917-d1a21dc52181" />
1. Click the **Enable CodeQL** button to accept the default configuration.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/6d5f7164-d8ed-4b5d-bbcf-8aed9e7acc5d" />
1. Below the **Tools** section. Verify **Copilot Autofix** is set to `On`.
<img width="350" alt="image" src="https://github.com/user-attachments/assets/b9d57e7a-f392-4c51-b137-f205a77adb79" />
## ⌨️ Activity: Provide a safe path for security findings
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.
1. In the top navigation, select the **Settings** tab.
1. In the left navigation, select **Advanced Security**.
1. Find the **Private vulnerability reporting** setting and verify it is `enabled`.
1. At the top navigation, click the **Security** tab.
1. In the left navigation, click the **Policy** option.
1. Click the **Start setup** button. An editor will be started to create the file `SECURITY.md`.
<img width="350" alt="" src="https://github.com/user-attachments/assets/183b9fcc-1521-47fd-8165-b476a8ccb370"/><br/>
<img width="350" alt="" src="https://github.com/user-attachments/assets/36c272d1-bc4a-48c8-b234-56173a214cdb"/>
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`.
1. We will ignore the provided template and instead use a recommendation from Mergington High School's IT department. Add the following content:
> 💡**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.
```markdown
# Mergington High School Security Policy
## Reporting a Vulnerability
At Mergington High, we take the security of our Extra-Curricular Activities website seriously, especially
since it contains student information. If you discover a security vulnerability, please follow these steps:
1. **Do not** create an issue on this repository, disclose the vulnerability publicly, or discuss it with other teachers/students.
1. In the top navigation of this repository, click the **Security** tab.
1. In the top right, click the **Report a vulnerability** button.
1. Fill out the provided form. It will request information like:
- A description of the vulnerability
- Steps to reproduce the issue
- Potential impact on student data or website functionality
- Suggested fix (if you have one)
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.
## Response Timeline
- We will acknowledge receipt of your report within 2 school days
- We will provide an initial assessment within 5 school days
- Critical issues affecting student data will be addressed immediately
- We will create a private fork to solve the issue and invite you as a collaborator so you can see our progress and contribute.
## Thank You
Your help in keeping our school's digital resources secure is greatly appreciated!
Responsible disclosure of security vulnerabilities helps protect our entire school community.
```
1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch.
1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
@@ -1,0 +1,13 @@
# Step 5: Release
With all our preparations ready, it's time to release them!
## ⌨️ Activity: Merge our collaboration changes
1. In the top navigation, select the **Pull requests** tab.
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.
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.
1. With both pull requests merged, Mona will prepare the final review and acknowledge the exercise as finished! Nice work! You are all done! 🎉
@@ -1,0 +1,36 @@
## Review
_Congratulations, you've completed this exercise! You're all set for an awesome time collaborating with your fellow teachers!_
<img src="https://octodex.github.com/images/jetpacktocat.png" alt=celebrate width=150 align=right>
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!
Here is a snippet of something you can share:
- Protected our code from accidental mistakes with `.gitignore` and branch protections.
- Set clear guidelines for teacher contributions with `CONTRIBUTING.md` and `CODEOWNERS`.
- Established community standards with a Code of Conduct and structured issue templates.
- Prepared for the future security challenges by enabling automated scanning and providing safe submission procedures.
### What's next?
This exercise was meant to introduce you to many of the different areas of managing a repository. However, there is still more to learn!
Here are some additional exercises for a deeper dive:
- [Skills: Secure your repository supply chain](https://github.com/skills/secure-repository-supply-chain)
- [Skills: Introduction to CodeQL](https://github.com/skills/introduction-to-codeql)
- [Skills: Introduction to Secret Scanning](https://github.com/skills/introduction-to-secret-scanning)
Here are some useful references from the [GitHub Docs](https://docs.github.com/en):
- [How to ignore files](https://docs.github.com/en/get-started/git-basics/ignoring-files)
- [Template gitignore files](https://github.com/github/gitignore)
- [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)
- [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)
- [About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners)
- [Setting guidelines for contributors](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors)
- [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)
- [Configuring default setup for code scanning](https://docs.github.com/en/code-security/code-scanning/enabling-code-scanning/configuring-default-setup-for-code-scanning)
- [Adding a security policy](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository)
@@ -1,0 +1,79 @@
name: Step 0
on:
push:
branches:
- main
permissions:
contents: write
actions: write
issues: write
env:
STEP_1_FILE: ".github/steps/1-protect-your-code.md"
jobs:
start_exercise:
if: |
!github.event.repository.is_template
name: Start Exercise
uses: skills/exercise-toolkit/.github/workflows/start-exercise.yml@v0.3.0
with:
exercise-title: "Introduction to Repository Management"
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."
post_next_step_content:
name: Post next step content
runs-on: ubuntu-latest
needs: [start_exercise]
env:
ISSUE_URL: ${{ needs.start_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Configure Git user
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
- name: Build comment - add step content
id: build-comment
uses: skills/action-text-variables@v2
with:
template-file: ${{ env.STEP_1_FILE }}
template-vars: |
login: ${{ github.actor }}
full_repo_name: ${{ github.repository }}
- name: Create comment - add step content
run: |
gh issue comment "$ISSUE_URL" \
--body "$ISSUE_BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_BODY: ${{ steps.build-comment.outputs.updated-text }}
- name: Create comment - watching for progress
run: |
gh issue comment "$ISSUE_URL" \
--body-file "exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Disable current workflow and enable next one
run: |
gh workflow enable "Step 1" || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,0 +1,112 @@
name: Step 1
on:
push:
branches:
- prepare-to-collaborate
paths:
- ".gitignore"
permissions:
contents: write
actions: write
issues: write
env:
STEP_2_FILE: ".github/steps/2-prepare-to-collaborate.md"
jobs:
find_exercise:
if: |
!github.event.repository.is_template
name: Find Exercise Issue
uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0
check_step_work:
name: Check step work
runs-on: ubuntu-latest
needs: [find_exercise]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Build message - step finished
id: build-message-step-finish
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
template-vars: |
next_step_number: 2
- name: Update comment - step finished
run: |
gh issue comment "$ISSUE_URL" \
--body "$ISSUE_BODY" \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
post_next_step_content:
name: Post next step content
runs-on: ubuntu-latest
needs: [find_exercise, check_step_work]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Build message - add step content
id: build-message-add-step-content
uses: skills/action-text-variables@v2
with:
template-file: ${{ env.STEP_2_FILE }}
template-vars: |
login: ${{ github.actor }}
- name: Create comment - step results
run: |
gh issue comment "$ISSUE_URL" \
--body "$COMMENT_BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMENT_BODY: ${{ steps.build-message-add-step-content.outputs.updated-text }}
- name: Create comment - watching for progress
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Disable current workflow and enable next one
run: |
gh workflow disable "Step 1" || true
gh workflow enable "Step 2" || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,0 +1,197 @@
name: Step 2
on:
push:
branches:
- prepare-to-collaborate
paths:
- "CONTRIBUTING.md"
- "CODEOWNERS"
permissions:
contents: read
actions: write
issues: write
env:
STEP_3_FILE: ".github/steps/3-foster-healthy-growth.md"
jobs:
find_exercise:
if: |
!github.event.repository.is_template
name: Find Exercise Issue
uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0
check_step_work:
name: Check step work
runs-on: ubuntu-latest
needs: [find_exercise]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Update comment - checking work
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check CONTRIBUTING.md exists
id: check-contributing
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'CONTRIBUTING.md',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('CONTRIBUTING.md')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check CODEOWNERS exists
id: check-codeowners
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'CODEOWNERS',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('CODEOWNERS')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check all results
id: check-all-results
uses: actions/github-script@v7
with:
script: |
const checks = [
JSON.parse(process.env['check1']),
JSON.parse(process.env['check2']),
];
const result = checks.every(check => check.passed);
return result
env:
check1: ${{ steps.check-contributing.outputs.result }}
check2: ${{ steps.check-codeowners.outputs.result }}
- name: Build message - step results
id: build-message-step-results
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md
template-vars: >
{
"step_number": 3,
"passed": ${{ steps.check-all-results.outputs.result }},
"results_table": [
${{ steps.check-contributing.outputs.result }},
${{ steps.check-codeowners.outputs.result }}
]
}
- name: Create comment - step results
run: |
gh issue comment "$ISSUE_URL" \
--body "$COMMENT_BODY" \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }}
- name: Fail job if not all checks passed
if: steps.check-all-results.outputs.result == 'false'
run: exit 1
- name: Build message - step finished
id: build-message-step-finish
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
template-vars: |
next_step_number: 3
- name: Update comment - step finished
run: |
gh issue comment "$ISSUE_URL" \
--body "$ISSUE_BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
post_next_step_content:
name: Post next step content
runs-on: ubuntu-latest
needs: [find_exercise, check_step_work]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Create comment - add step content
run: |
gh issue comment "$ISSUE_URL" \
--body-file "$STEP_3_FILE"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create comment - watching for progress
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Disable current workflow and enable next one
run: |
gh workflow disable "Step 2" || true
gh workflow enable "Step 3" || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,0 +1,233 @@
name: Step 3
on:
push:
branches:
- add-issue-templates
- prepare-to-collaborate
paths:
- ".github/ISSUE_TEMPLATE/**"
- "CODE_OF_CONDUCT.md"
permissions:
contents: read
actions: write
issues: write
env:
STEP_4_FILE: ".github/steps/4-prepare-for-the-inevitable.md"
jobs:
find_exercise:
if: |
!github.event.repository.is_template
name: Find Exercise Issue
uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0
check_step_work:
name: Check step work
runs-on: ubuntu-latest
needs: [find_exercise]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout branch - prepare-to-collaborate
uses: actions/checkout@v4
with:
path: prepare-to-collaborate
ref: prepare-to-collaborate
- name: Checkout branch - add-issue-templates
uses: actions/checkout@v4
with:
path: add-issue-templates
ref: add-issue-templates
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Update comment - checking work
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for Code of Conduct
id: check-code-of-conduct
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'CODE_OF_CONDUCT.md',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('prepare-to-collaborate/CODE_OF_CONDUCT.md')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check for Bug Report Templates
id: check-bug-report-template
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'bug_report.md',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('add-issue-templates/.github/ISSUE_TEMPLATE/bug_report.md')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check for Feature Request Templates
id: check-feature-request-template
uses: actions/github-script@v7
continue-on-error: true
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'feature_request.md',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('add-issue-templates/.github/ISSUE_TEMPLATE/feature_request.md')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check all results
id: check-all-results
uses: actions/github-script@v7
with:
script: |
const checks = [
JSON.parse(process.env['check1']),
JSON.parse(process.env['check2']),
JSON.parse(process.env['check3'])
];
const result = checks.every(check => check.passed);
return result
env:
check1: ${{ steps.check-code-of-conduct.outputs.result }}
check2: ${{ steps.check-bug-report-template.outputs.result }}
check3: ${{ steps.check-feature-request-template.outputs.result }}
- name: Build message - step results
id: build-message-step-results
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md
template-vars: >
{
"step_number": 4,
"passed": ${{ steps.check-all-results.outputs.result }},
"results_table": [
${{ steps.check-code-of-conduct.outputs.result }},
${{ steps.check-bug-report-template.outputs.result }},
${{ steps.check-feature-request-template.outputs.result }}
]
}
- name: Create comment - step results
run: |
gh issue comment "$ISSUE_URL" \
--body "$COMMENT_BODY" \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }}
- name: Fail job if not all checks passed
if: steps.check-all-results.outputs.result == 'false'
run: exit 1
- name: Build message - step finished
id: build-message-step-finish
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
template-vars: |
next_step_number: 4
- name: Update comment - step finished
run: |
gh issue comment "$ISSUE_URL" \
--body "$ISSUE_BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
post_next_step_content:
name: Post next step content
needs: [find_exercise, check_step_work]
runs-on: ubuntu-latest
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Create comment - add step content
run: |
gh issue comment "$ISSUE_URL" \
--body-file "$STEP_4_FILE"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create comment - watching for progress
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Disable current workflow and enable next one
run: |
gh workflow disable "Step 3" || true
gh workflow enable "Step 4" || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,0 +1,197 @@
name: Step 4
on:
push:
branches:
- prepare-to-collaborate
paths:
- ".github/dependabot.yml"
- "SECURITY.md"
permissions:
contents: write
actions: write
issues: write
env:
STEP_5_FILE: ".github/steps/5-merge.md"
jobs:
find_exercise:
if: |
!github.event.repository.is_template
name: Find Exercise Issue
uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0
check_step_work:
name: Check step work
runs-on: ubuntu-latest
needs: [find_exercise]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Update comment - checking work
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for Dependabot config
id: check-dependabot-config
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'dependabot.yml',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('.github/dependabot.yml')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check for Security Policy
id: check-security-policy
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Result object to store the message
let result = {
name: 'SECURITY.md',
passed: true,
message: ''
}
// Check that file exists
if (!fs.existsSync('SECURITY.md')) {
result.passed = false;
result.message = 'File is missing.';
}
return result;
- name: Check all results
id: check-all-results
uses: actions/github-script@v7
with:
script: |
const checks = [
JSON.parse(process.env['check1']),
JSON.parse(process.env['check2'])
];
const result = checks.every(check => check.passed);
return result
env:
check1: ${{ steps.check-dependabot-config.outputs.result }}
check2: ${{ steps.check-security-policy.outputs.result }}
- name: Build message - step results
id: build-message-step-results
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md
template-vars: >
{
"step_number": 4,
"passed": ${{ steps.check-all-results.outputs.result }},
"results_table": [
${{ steps.check-dependabot-config.outputs.result }},
${{ steps.check-security-policy.outputs.result }}
]
}
- name: Create comment - step results
run: |
gh issue comment "$ISSUE_URL" \
--body "$COMMENT_BODY" \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }}
- name: Fail job if not all checks passed
if: steps.check-all-results.outputs.result == 'false'
run: exit 1
- name: Build message - step finished
id: build-message-step-finish
uses: skills/action-text-variables@v2
with:
template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md
template-vars: |
next_step_number: 5
- name: Update comment - step finished
run: |
gh issue comment "$ISSUE_URL" \
--body "$ISSUE_BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }}
post_next_step_content:
name: Post next step content
needs: [find_exercise, check_step_work]
runs-on: ubuntu-latest
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Create comment - add step content
run: |
gh issue comment "$ISSUE_URL" \
--body-file "$STEP_5_FILE"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create comment - watching for progress
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Disable current workflow and enable next one
run: |
gh workflow disable "Step 4" || true
gh workflow enable "Step 5" || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,0 +1,107 @@
name: Step 5
on:
pull_request:
branches:
- main
types:
- closed
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
permissions:
contents: write
actions: write
issues: write
env:
REVIEW_FILE: ".github/steps/x-review.md"
jobs:
find_exercise:
if: |
!github.event.repository.is_template
name: Find Exercise Issue
uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0
check_step_work:
name: Check step work
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
needs: [find_exercise]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get Exercise Toolkit
uses: actions/checkout@v4
with:
repository: skills/exercise-toolkit
path: exercise-toolkit
ref: v0.3.0
- name: Update comment - checking work
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update comment - step finished - final review next
run: |
gh issue comment "$ISSUE_URL" \
--body-file exercise-toolkit/markdown-templates/step-feedback/lesson-review.md \
--edit-last
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
post_review_content:
name: Post review content
runs-on: ubuntu-latest
needs: [find_exercise, check_step_work]
env:
ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create comment - add review content
run: |
gh issue comment "$ISSUE_URL" \
--body-file ${{ env.REVIEW_FILE }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finish_exercise:
name: Finish Exercise
needs: [find_exercise, post_review_content]
uses: skills/exercise-toolkit/.github/workflows/finish-exercise.yml@57ecd963a3e54e6b01a30ebab57d5202a7afca3f
with:
issue-url: ${{ needs.find_exercise.outputs.issue-url }}
update-readme-with-congratulations: false
disable_workflow:
name: Disable this workflow
needs: [find_exercise, post_review_content]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Disable current workflow
run: gh workflow disable "${{github.workflow}}" || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,0 +1,2 @@
from . import routers
from . import database
@@ -1,0 +1,189 @@
"""
MongoDB database configuration and setup for Mergington High School API
"""
from pymongo import MongoClient
from argon2 import PasswordHasher
client = MongoClient('mongodb://localhost:27017/')
db = client['mergington_high']
activities_collection = db['activities']
teachers_collection = db['teachers']
def hash_password(password):
"""Hash password using Argon2"""
ph = PasswordHasher()
return ph.hash(password)
def init_database():
"""Initialize database if empty"""
if activities_collection.count_documents({}) == 0:
for name, details in initial_activities.items():
activities_collection.insert_one({"_id": name, **details})
if teachers_collection.count_documents({}) == 0:
for teacher in initial_teachers:
teachers_collection.insert_one({"_id": teacher["username"], **teacher})
initial_activities = {
"Chess Club": {
"description": "Learn strategies and compete in chess tournaments",
"schedule": "Mondays and Fridays, 3:15 PM - 4:45 PM",
"schedule_details": {
"days": ["Monday", "Friday"],
"start_time": "15:15",
"end_time": "16:45"
},
"max_participants": 12,
"participants": ["michael@mergington.edu", "daniel@mergington.edu"]
},
"Programming Class": {
"description": "Learn programming fundamentals and build software projects",
"schedule": "Tuesdays and Thursdays, 7:00 AM - 8:00 AM",
"schedule_details": {
"days": ["Tuesday", "Thursday"],
"start_time": "07:00",
"end_time": "08:00"
},
"max_participants": 20,
"participants": ["emma@mergington.edu", "sophia@mergington.edu"]
},
"Morning Fitness": {
"description": "Early morning physical training and exercises",
"schedule": "Mondays, Wednesdays, Fridays, 6:30 AM - 7:45 AM",
"schedule_details": {
"days": ["Monday", "Wednesday", "Friday"],
"start_time": "06:30",
"end_time": "07:45"
},
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
"Soccer Team": {
"description": "Join the school soccer team and compete in matches",
"schedule": "Tuesdays and Thursdays, 3:30 PM - 5:30 PM",
"schedule_details": {
"days": ["Tuesday", "Thursday"],
"start_time": "15:30",
"end_time": "17:30"
},
"max_participants": 22,
"participants": ["liam@mergington.edu", "noah@mergington.edu"]
},
"Basketball Team": {
"description": "Practice and compete in basketball tournaments",
"schedule": "Wednesdays and Fridays, 3:15 PM - 5:00 PM",
"schedule_details": {
"days": ["Wednesday", "Friday"],
"start_time": "15:15",
"end_time": "17:00"
},
"max_participants": 15,
"participants": ["ava@mergington.edu", "mia@mergington.edu"]
},
"Art Club": {
"description": "Explore various art techniques and create masterpieces",
"schedule": "Thursdays, 3:15 PM - 5:00 PM",
"schedule_details": {
"days": ["Thursday"],
"start_time": "15:15",
"end_time": "17:00"
},
"max_participants": 15,
"participants": ["amelia@mergington.edu", "harper@mergington.edu"]
},
"Drama Club": {
"description": "Act, direct, and produce plays and performances",
"schedule": "Mondays and Wednesdays, 3:30 PM - 5:30 PM",
"schedule_details": {
"days": ["Monday", "Wednesday"],
"start_time": "15:30",
"end_time": "17:30"
},
"max_participants": 20,
"participants": ["ella@mergington.edu", "scarlett@mergington.edu"]
},
"Math Club": {
"description": "Solve challenging problems and prepare for math competitions",
"schedule": "Tuesdays, 7:15 AM - 8:00 AM",
"schedule_details": {
"days": ["Tuesday"],
"start_time": "07:15",
"end_time": "08:00"
},
"max_participants": 10,
"participants": ["james@mergington.edu", "benjamin@mergington.edu"]
},
"Debate Team": {
"description": "Develop public speaking and argumentation skills",
"schedule": "Fridays, 3:30 PM - 5:30 PM",
"schedule_details": {
"days": ["Friday"],
"start_time": "15:30",
"end_time": "17:30"
},
"max_participants": 12,
"participants": ["charlotte@mergington.edu", "amelia@mergington.edu"]
},
"Weekend Robotics Workshop": {
"description": "Build and program robots in our state-of-the-art workshop",
"schedule": "Saturdays, 10:00 AM - 2:00 PM",
"schedule_details": {
"days": ["Saturday"],
"start_time": "10:00",
"end_time": "14:00"
},
"max_participants": 15,
"participants": ["ethan@mergington.edu", "oliver@mergington.edu"]
},
"Science Olympiad": {
"description": "Weekend science competition preparation for regional and state events",
"schedule": "Saturdays, 1:00 PM - 4:00 PM",
"schedule_details": {
"days": ["Saturday"],
"start_time": "13:00",
"end_time": "16:00"
},
"max_participants": 18,
"participants": ["isabella@mergington.edu", "lucas@mergington.edu"]
},
"Sunday Chess Tournament": {
"description": "Weekly tournament for serious chess players with rankings",
"schedule": "Sundays, 2:00 PM - 5:00 PM",
"schedule_details": {
"days": ["Sunday"],
"start_time": "14:00",
"end_time": "17:00"
},
"max_participants": 16,
"participants": ["william@mergington.edu", "jacob@mergington.edu"]
}
}
initial_teachers = [
{
"username": "mrodriguez",
"display_name": "Ms. Rodriguez",
"password": hash_password("art123"),
"role": "teacher"
},
{
"username": "mchen",
"display_name": "Mr. Chen",
"password": hash_password("chess456"),
"role": "teacher"
},
{
"username": "principal",
"display_name": "Principal Martinez",
"password": hash_password("admin789"),
"role": "admin"
}
]
@@ -1,0 +1,868 @@
document.addEventListener("DOMContentLoaded", () => {
const activitiesList = document.getElementById("activities-list");
const messageDiv = document.getElementById("message");
const registrationModal = document.getElementById("registration-modal");
const modalActivityName = document.getElementById("modal-activity-name");
const signupForm = document.getElementById("signup-form");
const activityInput = document.getElementById("activity");
const closeRegistrationModal = document.querySelector(".close-modal");
const searchInput = document.getElementById("activity-search");
const searchButton = document.getElementById("search-button");
const categoryFilters = document.querySelectorAll(".category-filter");
const dayFilters = document.querySelectorAll(".day-filter");
const timeFilters = document.querySelectorAll(".time-filter");
const loginButton = document.getElementById("login-button");
const userInfo = document.getElementById("user-info");
const displayName = document.getElementById("display-name");
const logoutButton = document.getElementById("logout-button");
const loginModal = document.getElementById("login-modal");
const loginForm = document.getElementById("login-form");
const closeLoginModal = document.querySelector(".close-login-modal");
const loginMessage = document.getElementById("login-message");
const activityTypes = {
sports: { label: "Sports", color: "#e8f5e9", textColor: "#2e7d32" },
arts: { label: "Arts", color: "#f3e5f5", textColor: "#7b1fa2" },
academic: { label: "Academic", color: "#e3f2fd", textColor: "#1565c0" },
community: { label: "Community", color: "#fff3e0", textColor: "#e65100" },
technology: { label: "Technology", color: "#e8eaf6", textColor: "#3949ab" },
};
let allActivities = {};
let currentFilter = "all";
let searchQuery = "";
let currentDay = "";
let currentTimeRange = "";
let currentUser = null;
const timeRanges = {
morning: { start: "06:00", end: "08:00" },
afternoon: { start: "15:00", end: "18:00" },
weekend: { days: ["Saturday", "Sunday"] },
};
function initializeFilters() {
const activeDayFilter = document.querySelector(".day-filter.active");
if (activeDayFilter) {
currentDay = activeDayFilter.dataset.day;
}
const activeTimeFilter = document.querySelector(".time-filter.active");
if (activeTimeFilter) {
currentTimeRange = activeTimeFilter.dataset.time;
}
}
function setDayFilter(day) {
currentDay = day;
dayFilters.forEach((btn) => {
if (btn.dataset.day === day) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
fetchActivities();
}
function setTimeRangeFilter(timeRange) {
currentTimeRange = timeRange;
timeFilters.forEach((btn) => {
if (btn.dataset.time === timeRange) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
fetchActivities();
}
function checkAuthentication() {
const savedUser = localStorage.getItem("currentUser");
if (savedUser) {
try {
currentUser = JSON.parse(savedUser);
updateAuthUI();
validateUserSession(currentUser.username);
} catch (error) {
console.error("Error parsing saved user", error);
logout();
}
}
updateAuthBodyClass();
}
async function validateUserSession(username) {
try {
const response = await fetch(
`/auth/check-session?username=${encodeURIComponent(username)}`
);
if (!response.ok) {
logout();
return;
}
const userData = await response.json();
currentUser = userData;
localStorage.setItem("currentUser", JSON.stringify(userData));
updateAuthUI();
} catch (error) {
console.error("Error validating session:", error);
}
}
function updateAuthUI() {
if (currentUser) {
loginButton.classList.add("hidden");
userInfo.classList.remove("hidden");
displayName.textContent = currentUser.display_name;
} else {
loginButton.classList.remove("hidden");
userInfo.classList.add("hidden");
displayName.textContent = "";
}
updateAuthBodyClass();
fetchActivities();
}
function updateAuthBodyClass() {
if (currentUser) {
document.body.classList.remove("not-authenticated");
} else {
document.body.classList.add("not-authenticated");
}
}
async function login(username, password) {
try {
const response = await fetch(
`/auth/login?username=${encodeURIComponent(
username
)}&password=${encodeURIComponent(password)}`,
{
method: "POST",
}
);
const data = await response.json();
if (!response.ok) {
showLoginMessage(
data.detail || "Invalid username or password",
"error"
);
return false;
}
currentUser = data;
localStorage.setItem("currentUser", JSON.stringify(data));
updateAuthUI();
closeLoginModalHandler();
showMessage(`Welcome, ${currentUser.display_name}!`, "success");
return true;
} catch (error) {
console.error("Error during login:", error);
showLoginMessage("Login failed. Please try again.", "error");
return false;
}
}
function logout() {
currentUser = null;
localStorage.removeItem("currentUser");
updateAuthUI();
showMessage("You have been logged out.", "info");
}
function showLoginMessage(text, type) {
loginMessage.textContent = text;
loginMessage.className = `message ${type}`;
loginMessage.classList.remove("hidden");
}
function openLoginModal() {
loginModal.classList.remove("hidden");
loginModal.classList.add("show");
loginMessage.classList.add("hidden");
loginForm.reset();
}
function closeLoginModalHandler() {
loginModal.classList.remove("show");
setTimeout(() => {
loginModal.classList.add("hidden");
loginForm.reset();
}, 300);
}
loginButton.addEventListener("click", openLoginModal);
logoutButton.addEventListener("click", logout);
closeLoginModal.addEventListener("click", closeLoginModalHandler);
window.addEventListener("click", (event) => {
if (event.target === loginModal) {
closeLoginModalHandler();
}
});
loginForm.addEventListener("submit", async (event) => {
event.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
await login(username, password);
});
function showLoadingSkeletons() {
activitiesList.innerHTML = "";
for (let i = 0; i < 9; i++) {
const skeletonCard = document.createElement("div");
skeletonCard.className = "skeleton-card";
skeletonCard.innerHTML = `
<div class="skeleton-line skeleton-title"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line skeleton-text short"></div>
<div style="margin-top: 8px;">
<div class="skeleton-line" style="height: 6px;"></div>
<div class="skeleton-line skeleton-text short" style="height: 8px; margin-top: 3px;"></div>
</div>
<div style="margin-top: auto;">
<div class="skeleton-line" style="height: 24px; margin-top: 8px;"></div>
</div>
`;
activitiesList.appendChild(skeletonCard);
}
}
function formatSchedule(details) {
if (details.schedule_details) {
const days = details.schedule_details.days.join(", ");
const formatTime = (time24) => {
const [hours, minutes] = time24.split(":").map((num) => parseInt(num));
const period = hours >= 12 ? "PM" : "AM";
const displayHours = hours % 12 || 12;
return `${displayHours}:${minutes
.toString()
.padStart(2, "0")} ${period}`;
};
const startTime = formatTime(details.schedule_details.start_time);
const endTime = formatTime(details.schedule_details.end_time);
return `${days}, ${startTime} - ${endTime}`;
}
return details.schedule;
}
function getActivityType(activityName, description) {
const name = activityName.toLowerCase();
const desc = description.toLowerCase();
if (
name.includes("soccer") ||
name.includes("basketball") ||
name.includes("sport") ||
name.includes("fitness") ||
desc.includes("team") ||
desc.includes("game") ||
desc.includes("athletic")
) {
return "sports";
} else if (
name.includes("art") ||
name.includes("music") ||
name.includes("theater") ||
name.includes("drama") ||
desc.includes("creative") ||
desc.includes("paint")
) {
return "arts";
} else if (
name.includes("science") ||
name.includes("math") ||
name.includes("academic") ||
name.includes("study") ||
name.includes("olympiad") ||
desc.includes("learning") ||
desc.includes("education") ||
desc.includes("competition")
) {
return "academic";
} else if (
name.includes("volunteer") ||
name.includes("community") ||
desc.includes("service") ||
desc.includes("volunteer")
) {
return "community";
} else if (
name.includes("computer") ||
name.includes("coding") ||
name.includes("tech") ||
name.includes("robotics") ||
desc.includes("programming") ||
desc.includes("technology") ||
desc.includes("digital") ||
desc.includes("robot")
) {
return "technology";
}
return "academic";
}
async function fetchActivities() {
showLoadingSkeletons();
try {
let queryParams = [];
if (currentDay) {
queryParams.push(`day=${encodeURIComponent(currentDay)}`);
}
if (currentTimeRange) {
const range = timeRanges[currentTimeRange];
if (currentTimeRange === "weekend") {
} else if (range) {
queryParams.push(`start_time=${encodeURIComponent(range.start)}`);
queryParams.push(`end_time=${encodeURIComponent(range.end)}`);
}
}
const queryString =
queryParams.length > 0 ? `?${queryParams.join("&")}` : "";
const response = await fetch(`/activities${queryString}`);
const activities = await response.json();
allActivities = activities;
displayFilteredActivities();
} catch (error) {
activitiesList.innerHTML =
"<p>Failed to load activities. Please try again later.</p>";
console.error("Error fetching activities:", error);
}
}
function displayFilteredActivities() {
activitiesList.innerHTML = "";
let filteredActivities = {};
Object.entries(allActivities).forEach(([name, details]) => {
const activityType = getActivityType(name, details.description);
if (currentFilter !== "all" && activityType !== currentFilter) {
return;
}
if (currentTimeRange === "weekend" && details.schedule_details) {
const activityDays = details.schedule_details.days;
const isWeekendActivity = activityDays.some((day) =>
timeRanges.weekend.days.includes(day)
);
if (!isWeekendActivity) {
return;
}
}
const searchableContent = [
name.toLowerCase(),
details.description.toLowerCase(),
formatSchedule(details).toLowerCase(),
].join(" ");
if (
searchQuery &&
!searchableContent.includes(searchQuery.toLowerCase())
) {
return;
}
filteredActivities[name] = details;
});
if (Object.keys(filteredActivities).length === 0) {
activitiesList.innerHTML = `
<div class="no-results">
<h4>No activities found</h4>
<p>Try adjusting your search or filter criteria</p>
</div>
`;
return;
}
Object.entries(filteredActivities).forEach(([name, details]) => {
renderActivityCard(name, details);
});
}
function renderActivityCard(name, details) {
const activityCard = document.createElement("div");
activityCard.className = "activity-card";
const totalSpots = details.max_participants;
const takenSpots = details.participants.length;
const spotsLeft = totalSpots - takenSpots;
const capacityPercentage = (takenSpots / totalSpots) * 100;
const isFull = spotsLeft <= 0;
let capacityStatusClass = "capacity-available";
if (isFull) {
capacityStatusClass = "capacity-full";
} else if (capacityPercentage >= 75) {
capacityStatusClass = "capacity-near-full";
}
const activityType = getActivityType(name, details.description);
const typeInfo = activityTypes[activityType];
const formattedSchedule = formatSchedule(details);
const tagHtml = `
<span class="activity-tag" style="background-color: ${typeInfo.color}; color: ${typeInfo.textColor}">
${typeInfo.label}
</span>
`;
const capacityIndicator = `
<div class="capacity-container ${capacityStatusClass}">
<div class="capacity-bar-bg">
<div class="capacity-bar-fill" style="width: ${capacityPercentage}%"></div>
</div>
<div class="capacity-text">
<span>${takenSpots} enrolled</span>
<span>${spotsLeft} spots left</span>
</div>
</div>
`;
activityCard.innerHTML = `
${tagHtml}
<h4>${name}</h4>
<p>${details.description}</p>
<p class="tooltip">
<strong>Schedule:</strong> ${formattedSchedule}
<span class="tooltip-text">Regular meetings at this time throughout the semester</span>
</p>
${capacityIndicator}
<div class="participants-list">
<h5>Current Participants:</h5>
<ul>
${details.participants
.map(
(email) => `
<li>
${email}
${
currentUser
? `
<span class="delete-participant tooltip" data-activity="${name}" data-email="${email}">
✖
<span class="tooltip-text">Unregister this student</span>
</span>
`
: ""
}
</li>
`
)
.join("")}
</ul>
</div>
<div class="activity-card-actions">
${
currentUser
? `
<button class="register-button" data-activity="${name}" ${
isFull ? "disabled" : ""
}>
${isFull ? "Activity Full" : "Register Student"}
</button>
`
: `
<div class="auth-notice">
Teachers can register students.
</div>
`
}
</div>
`;
const deleteButtons = activityCard.querySelectorAll(".delete-participant");
deleteButtons.forEach((button) => {
button.addEventListener("click", handleUnregister);
});
if (currentUser) {
const registerButton = activityCard.querySelector(".register-button");
if (!isFull) {
registerButton.addEventListener("click", () => {
openRegistrationModal(name);
});
}
}
activitiesList.appendChild(activityCard);
}
searchInput.addEventListener("input", (event) => {
searchQuery = event.target.value;
displayFilteredActivities();
});
searchButton.addEventListener("click", (event) => {
event.preventDefault();
searchQuery = searchInput.value;
displayFilteredActivities();
});
categoryFilters.forEach((button) => {
button.addEventListener("click", () => {
categoryFilters.forEach((btn) => btn.classList.remove("active"));
button.classList.add("active");
currentFilter = button.dataset.category;
displayFilteredActivities();
});
});
dayFilters.forEach((button) => {
button.addEventListener("click", () => {
dayFilters.forEach((btn) => btn.classList.remove("active"));
button.classList.add("active");
currentDay = button.dataset.day;
fetchActivities();
});
});
timeFilters.forEach((button) => {
button.addEventListener("click", () => {
timeFilters.forEach((btn) => btn.classList.remove("active"));
button.classList.add("active");
currentTimeRange = button.dataset.time;
fetchActivities();
});
});
function openRegistrationModal(activityName) {
modalActivityName.textContent = activityName;
activityInput.value = activityName;
registrationModal.classList.remove("hidden");
setTimeout(() => {
registrationModal.classList.add("show");
}, 10);
}
function closeRegistrationModalHandler() {
registrationModal.classList.remove("show");
setTimeout(() => {
registrationModal.classList.add("hidden");
signupForm.reset();
}, 300);
}
closeRegistrationModal.addEventListener(
"click",
closeRegistrationModalHandler
);
window.addEventListener("click", (event) => {
if (event.target === registrationModal) {
closeRegistrationModalHandler();
}
});
function showConfirmationDialog(message, confirmCallback) {
let confirmDialog = document.getElementById("confirm-dialog");
if (!confirmDialog) {
confirmDialog = document.createElement("div");
confirmDialog.id = "confirm-dialog";
confirmDialog.className = "modal hidden";
confirmDialog.innerHTML = `
<div class="modal-content">
<h3>Confirm Action</h3>
<p id="confirm-message"></p>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
<button id="cancel-button" class="cancel-btn">Cancel</button>
<button id="confirm-button" class="confirm-btn">Confirm</button>
</div>
</div>
`;
document.body.appendChild(confirmDialog);
const cancelBtn = confirmDialog.querySelector("#cancel-button");
const confirmBtn = confirmDialog.querySelector("#confirm-button");
cancelBtn.style.backgroundColor = "#f1f1f1";
cancelBtn.style.color = "#333";
confirmBtn.style.backgroundColor = "#dc3545";
confirmBtn.style.color = "white";
}
const confirmMessage = document.getElementById("confirm-message");
confirmMessage.textContent = message;
confirmDialog.classList.remove("hidden");
setTimeout(() => {
confirmDialog.classList.add("show");
}, 10);
const cancelButton = document.getElementById("cancel-button");
const confirmButton = document.getElementById("confirm-button");
const newCancelButton = cancelButton.cloneNode(true);
const newConfirmButton = confirmButton.cloneNode(true);
cancelButton.parentNode.replaceChild(newCancelButton, cancelButton);
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
newCancelButton.addEventListener("click", () => {
confirmDialog.classList.remove("show");
setTimeout(() => {
confirmDialog.classList.add("hidden");
}, 300);
});
newConfirmButton.addEventListener("click", () => {
confirmCallback();
confirmDialog.classList.remove("show");
setTimeout(() => {
confirmDialog.classList.add("hidden");
}, 300);
});
confirmDialog.addEventListener("click", (event) => {
if (event.target === confirmDialog) {
confirmDialog.classList.remove("show");
setTimeout(() => {
confirmDialog.classList.add("hidden");
}, 300);
}
});
}
async function handleUnregister(event) {
if (!currentUser) {
showMessage(
"You must be logged in as a teacher to unregister students.",
"error"
);
return;
}
const activity = event.target.dataset.activity;
const email = event.target.dataset.email;
showConfirmationDialog(
`Are you sure you want to unregister ${email} from ${activity}?`,
async () => {
try {
const response = await fetch(
`/activities/${encodeURIComponent(
activity
)}/unregister?email=${encodeURIComponent(
email
)}&teacher_username=${encodeURIComponent(currentUser.username)}`,
{
method: "POST",
}
);
const result = await response.json();
if (response.ok) {
showMessage(result.message, "success");
fetchActivities();
} else {
showMessage(result.detail || "An error occurred", "error");
}
} catch (error) {
showMessage("Failed to unregister. Please try again.", "error");
console.error("Error unregistering:", error);
}
}
);
}
function showMessage(text, type) {
messageDiv.textContent = text;
messageDiv.className = `message ${type}`;
messageDiv.classList.remove("hidden");
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
}
signupForm.addEventListener("submit", async (event) => {
event.preventDefault();
if (!currentUser) {
showMessage(
"You must be logged in as a teacher to register students.",
"error"
);
return;
}
const email = document.getElementById("email").value;
const activity = activityInput.value;
try {
const response = await fetch(
`/activities/${encodeURIComponent(
activity
)}/signup?email=${encodeURIComponent(
email
)}&teacher_username=${encodeURIComponent(currentUser.username)}`,
{
method: "POST",
}
);
const result = await response.json();
if (response.ok) {
showMessage(result.message, "success");
closeRegistrationModalHandler();
fetchActivities();
} else {
showMessage(result.detail || "An error occurred", "error");
}
} catch (error) {
showMessage("Failed to sign up. Please try again.", "error");
console.error("Error signing up:", error);
}
});
window.activityFilters = {
setDayFilter,
setTimeRangeFilter,
};
checkAuthentication();
initializeFilters();
fetchActivities();
});
@@ -1,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mergington High School Activities</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header>
<h1>Mergington High School</h1>
<h2>Extracurricular Activities</h2>
<div id="user-controls">
<div id="user-status">
<button id="login-button" class="icon-button">
<span class="user-icon">👤</span>
<span>Login</span>
</button>
<div id="user-info" class="hidden">
<span id="display-name"></span>
<button id="logout-button">Logout</button>
</div>
</div>
</div>
</header>
<main>
<section id="activities-container">
<div class="main-content-layout">
<aside class="sidebar-filters">
<h3>Filter Activities</h3>
<div class="search-box">
<input
type="text"
id="activity-search"
placeholder="Search activities..."
/>
<button id="search-button" aria-label="Search">
<span class="search-icon">🔍</span>
</button>
</div>
<div class="filter-container">
<div class="filter-label">Filter by category:</div>
<div class="category-filters" id="category-filters">
<button class="category-filter active" data-category="all">
All
</button>
<button class="category-filter" data-category="sports">
Sports
</button>
<button class="category-filter" data-category="arts">
Arts
</button>
<button class="category-filter" data-category="academic">
Academic
</button>
<button class="category-filter" data-category="community">
Community
</button>
<button class="category-filter" data-category="technology">
Technology
</button>
</div>
</div>
<div class="filter-container day-filter-container">
<div class="filter-label">Filter by day:</div>
<div class="day-filters" id="day-filters">
<button class="day-filter active" data-day="">All Days</button>
<button class="day-filter" data-day="Monday">Monday</button>
<button class="day-filter" data-day="Tuesday">Tuesday</button>
<button class="day-filter" data-day="Wednesday">
Wednesday
</button>
<button class="day-filter" data-day="Thursday">Thursday</button>
<button class="day-filter" data-day="Friday">Friday</button>
<button class="day-filter" data-day="Saturday">Saturday</button>
<button class="day-filter" data-day="Sunday">Sunday</button>
</div>
</div>
<div class="filter-container time-filter-container">
<div class="filter-label">Filter by time:</div>
<div class="time-filters">
<button class="time-filter active" data-time="">
All Times
</button>
<button class="time-filter" data-time="morning">
Before School
</button>
<button class="time-filter" data-time="afternoon">
After School
</button>
<button class="time-filter" data-time="weekend">Weekend</button>
</div>
</div>
</aside>
<div class="activities-content">
<div id="activities-list">
<p>Loading activities...</p>
</div>
<div id="message" class="hidden message"></div>
</div>
</div>
</section>
</main>
<footer>
<p>© 2023 Mergington High School</p>
</footer>
<div id="registration-modal" class="modal hidden">
<div class="modal-content">
<span class="close-modal">×</span>
<h3>Register for <span id="modal-activity-name"></span></h3>
<form id="signup-form">
<div class="form-group">
<label for="email">Student Email:</label>
<input
type="email"
id="email"
required
placeholder="your-email@mergington.edu"
/>
</div>
<input type="hidden" id="activity" value="" />
<button type="submit">Register</button>
</form>
</div>
</div>
<div id="login-modal" class="modal hidden">
<div class="modal-content">
<span class="close-login-modal">×</span>
<h3>Teacher Login</h3>
<form id="login-form">
<div class="form-group">
<label for="username">Username:</label>
<input
type="text"
id="username"
required
placeholder="Enter your username"
/>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input
type="password"
id="password"
required
placeholder="Enter your password"
/>
</div>
<button type="submit">Login</button>
</form>
<div id="login-message" class="hidden message"></div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
@@ -1,0 +1,666 @@
:root {
--primary: #1a237e;
--primary-light: #534bae;
--primary-dark: #000051;
--primary-text: #ffffff;
--secondary: #ff6f00;
--secondary-light: #ffa040;
--secondary-dark: #c43e00;
--secondary-text: #ffffff;
--background: #f5f5f5;
--surface: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border: #e0e0e0;
--border-light: #f0f0f0;
--border-focus: #d0d0d0;
--success: #2e7d32;
--success-light: #e8f5e9;
--warning: #ff9800;
--warning-light: #fff3cd;
--error: #c62828;
--error-light: #ffebee;
--info: #0c5460;
--info-light: #d1ecf1;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
body {
font-family: Arial, sans-serif;
line-height: 1.4;
color: var(--text-primary);
max-width: 1200px;
margin: 0 auto;
padding: 12px;
background-color: var(--background);
font-size: 0.9rem;
}
header {
text-align: center;
padding: 12px 0;
margin-bottom: 15px;
background-color: var(--primary);
color: var(--primary-text);
border-radius: 5px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
header h1 {
margin-bottom: 5px;
font-size: 1.6rem;
}
header h2 {
font-size: 1.2rem;
}
main {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
section {
background-color: var(--surface);
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
width: 100%;
}
section h3 {
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
color: var(--primary);
font-size: 1.1rem;
}
.main-content-layout {
display: flex;
flex-direction: column;
gap: 15px;
}
.sidebar-filters {
padding: 15px;
background-color: var(--surface);
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.sidebar-filters h3 {
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
color: var(--primary);
font-size: 1.1rem;
}
.activities-content {
flex: 1;
}
@media (min-width: 768px) {
.main-content-layout {
flex-direction: row;
align-items: flex-start;
}
.sidebar-filters {
width: 200px;
position: sticky;
top: 15px;
max-height: calc(100vh - 30px);
overflow-y: auto;
}
.activities-content {
flex: 1;
}
}
#activities-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
width: 100%;
}
.activity-card {
padding: 12px;
border: 1px solid var(--border);
border-radius: 6px;
background-color: var(--surface);
display: flex;
flex-direction: column;
height: 100%;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
font-size: 0.85rem;
}
.activity-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
border-color: var(--border-focus);
}
.activity-card h4 {
margin-bottom: 8px;
color: var(--primary);
font-size: 1rem;
letter-spacing: 0.3px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-light);
}
.activity-card p {
margin-bottom: 8px;
line-height: 1.4;
}
.activity-card-actions {
margin-top: auto;
padding-top: 10px;
display: flex;
justify-content: center;
}
.activity-tag {
position: absolute;
top: 8px;
right: 8px;
background: #e8eaf6;
color: #3949ab;
font-size: 0.65rem;
font-weight: bold;
padding: 2px 6px;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.capacity-container {
margin: 8px 0;
width: 100%;
}
.capacity-bar-bg {
height: 6px;
background-color: var(--border-light);
border-radius: 3px;
overflow: hidden;
}
.capacity-text {
display: flex;
justify-content: space-between;
margin-top: 3px;
font-size: 0.7rem;
color: var(--text-secondary);
}
.capacity-full .capacity-bar-fill {
background-color: var(--error);
}
.capacity-near-full .capacity-bar-fill {
background-color: var(--warning);
}
.capacity-available .capacity-bar-fill {
background-color: var(--success);
}
.participants-list {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-light);
}
.participants-list h5 {
color: var(--primary);
margin-bottom: 5px;
font-size: 0.8em;
}
.participants-list ul {
list-style-type: none;
padding-left: 0;
margin: 0;
max-height: 100px;
}
.participants-list li {
padding: 2px 0;
color: var(--text-secondary);
font-size: 0.8em;
display: flex;
justify-content: space-between;
align-items: center;
}
.register-button {
background: linear-gradient(145deg, var(--secondary), var(--secondary-dark));
color: var(--secondary-text);
margin-top: 10px;
padding: 6px 12px;
width: 100%;
font-weight: bold;
border-radius: 20px;
box-shadow: 0 2px 4px rgba(255, 111, 0, 0.2);
transition: all 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.85rem;
letter-spacing: 0.3px;
text-transform: uppercase;
border: none;
}
button {
background-color: var(--primary);
color: white;
border: none;
padding: 6px 12px;
font-size: 0.85rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip .tooltip-text {
visibility: hidden;
background-color: rgba(33, 33, 33, 0.9);
color: #fff;
text-align: center;
padding: 8px 12px;
border-radius: 4px;
font-size: 0.8rem;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
width: max-content;
max-width: 250px;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.delete-participant {
cursor: pointer;
}
.delete-participant .tooltip-text {
left: auto;
right: calc(100% + 8px);
top: 50%;
bottom: auto;
transform: translateY(-50%);
white-space: nowrap;
}
.skeleton-card {
padding: 12px;
border: 1px solid var(--border);
border-radius: 6px;
background-color: var(--surface);
display: flex;
flex-direction: column;
height: 180px;
position: relative;
overflow: hidden;
}
.skeleton-line {
height: 10px;
margin-bottom: 8px;
background: linear-gradient(
90deg,
var(--border-light) 25%,
var(--border) 50%,
var(--border-light) 75%
);
background-size: 200% 100%;
border-radius: 3px;
animation: shimmer 1.5s infinite;
}
.skeleton-title {
height: 18px;
width: 70%;
margin-bottom: 10px;
}
.skeleton-text {
width: 100%;
}
.skeleton-text.short {
width: 60%;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.show {
opacity: 1;
}
.modal-content {
background-color: var(--surface);
padding: 18px;
border-radius: 5px;
width: 90%;
max-width: 350px;
position: relative;
transform: translateY(-20px);
transition: transform 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid var(--border);
}
.modal.show .modal-content {
transform: translateY(0);
}
.close-modal,
.close-login-modal {
position: absolute;
right: 12px;
top: 8px;
font-size: 18px;
cursor: pointer;
color: var(--text-secondary);
line-height: 1;
}
#login-modal .modal-content h3 {
color: var(--primary);
font-size: 1.2rem;
margin-bottom: 15px;
text-align: center;
}
.hidden {
display: none !important;
}
.no-results {
text-align: center;
padding: 12px;
color: var(--text-secondary);
background-color: var(--surface);
border-radius: 6px;
border: 1px dashed var(--border);
margin: 10px 0;
font-size: 0.85rem;
}
footer {
text-align: center;
margin-top: 20px;
padding: 10px;
color: var(--text-secondary);
font-size: 0.8rem;
}
.search-box {
display: flex;
width: 100%;
margin-bottom: 15px;
}
.search-box input {
flex: 1;
min-width: 0;
padding: 6px 10px;
border: 1px solid var(--border);
border-right: none;
border-radius: 20px 0 0 20px;
font-size: 0.85rem;
transition: all 0.3s ease;
}
.search-box input:focus {
outline: none;
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(83, 75, 174, 0.1);
}
.search-box button {
width: 36px;
height: 100%;
min-height: 32px;
background: var(--primary);
color: white;
border: none;
border-radius: 0 20px 20px 0;
cursor: pointer;
transition: background-color 0.3s ease;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.search-box button:hover {
background-color: var(--primary-light);
}
.search-icon {
font-size: 1rem;
}
.filter-container {
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-label {
font-weight: 600;
color: var(--text-primary);
font-size: 0.8rem;
}
.category-filters,
.day-filters,
.time-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.category-filter,
.day-filter,
.time-filter {
background-color: var(--background);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 4px 10px;
border-radius: 15px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.category-filter.active,
.day-filter.active,
.time-filter.active {
background-color: var(--primary);
color: white;
border-color: var(--primary-dark);
}
.category-filter:hover,
.day-filter:hover,
.time-filter:hover {
background-color: var(--primary-light);
color: white;
}
.time-filters {
width: 100%;
}
.reset-button {
background-color: var(--border);
color: var(--text-primary);
padding: 5px 12px;
border-radius: 15px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
align-self: flex-start;
}
.reset-button:hover {
background-color: var(--border-focus);
}
@media (max-width: 767px) {
.sidebar-filters {
margin-bottom: 15px;
}
.main-content-layout {
flex-direction: column;
}
#activities-list {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.sidebar-filters {
width: 100%;
}
}
#user-controls {
position: absolute;
top: 10px;
right: 15px;
}
#user-status {
display: flex;
align-items: center;
}
#user-info {
display: flex;
align-items: center;
gap: 10px;
}
#display-name {
margin-right: 5px;
font-weight: 500;
}
.icon-button {
display: flex;
align-items: center;
gap: 5px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 4px 12px;
font-size: 0.85rem;
transition: background-color 0.2s;
}
.icon-button:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.user-icon {
font-size: 1rem;
}
#logout-button {
padding: 3px 10px;
background-color: rgba(255, 255, 255, 0.2);
font-size: 0.8rem;
border-radius: 20px;
}
#logout-button:hover {
background-color: rgba(255, 255, 255, 0.3);
}
@@ -1,0 +1,2 @@
from . import activities
from . import auth
@@ -1,0 +1,127 @@
"""
Endpoints for the High School Management System API
"""
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import RedirectResponse
from typing import Dict, Any, Optional, List
from ..database import activities_collection, teachers_collection
router = APIRouter(
prefix="/activities",
tags=["activities"]
)
@router.get("/", response_model=Dict[str, Any])
def get_activities(
day: Optional[str] = None,
start_time: Optional[str] = None,
end_time: Optional[str] = None
) -> Dict[str, Any]:
"""
Get all activities with their details, with optional filtering by day and time
- day: Filter activities occurring on this day (e.g., 'Monday', 'Tuesday')
- start_time: Filter activities starting at or after this time (24-hour format, e.g., '14:30')
- end_time: Filter activities ending at or before this time (24-hour format, e.g., '17:00')
"""
query = {}
if day:
query["schedule_details.days"] = {"$in": [day]}
if start_time:
query["schedule_details.start_time"] = {"$gte": start_time}
if end_time:
query["schedule_details.end_time"] = {"$lte": end_time}
activities = {}
for activity in activities_collection.find(query):
name = activity.pop('_id')
activities[name] = activity
return activities
@router.get("/days", response_model=List[str])
def get_available_days() -> List[str]:
"""Get a list of all days that have activities scheduled"""
pipeline = [
{"$unwind": "$schedule_details.days"},
{"$group": {"_id": "$schedule_details.days"}},
{"$sort": {"_id": 1}}
]
days = []
for day_doc in activities_collection.aggregate(pipeline):
days.append(day_doc["_id"])
return days
@router.post("/{activity_name}/signup")
def signup_for_activity(activity_name: str, email: str, teacher_username: Optional[str] = Query(None)):
"""Sign up a student for an activity - requires teacher authentication"""
if not teacher_username:
raise HTTPException(status_code=401, detail="Authentication required for this action")
teacher = teachers_collection.find_one({"_id": teacher_username})
if not teacher:
raise HTTPException(status_code=401, detail="Invalid teacher credentials")
activity = activities_collection.find_one({"_id": activity_name})
if not activity:
raise HTTPException(status_code=404, detail="Activity not found")
if email in activity["participants"]:
raise HTTPException(
status_code=400, detail="Already signed up for this activity")
result = activities_collection.update_one(
{"_id": activity_name},
{"$push": {"participants": email}}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Failed to update activity")
return {"message": f"Signed up {email} for {activity_name}"}
@router.post("/{activity_name}/unregister")
def unregister_from_activity(activity_name: str, email: str, teacher_username: Optional[str] = Query(None)):
"""Remove a student from an activity - requires teacher authentication"""
if not teacher_username:
raise HTTPException(status_code=401, detail="Authentication required for this action")
teacher = teachers_collection.find_one({"_id": teacher_username})
if not teacher:
raise HTTPException(status_code=401, detail="Invalid teacher credentials")
activity = activities_collection.find_one({"_id": activity_name})
if not activity:
raise HTTPException(status_code=404, detail="Activity not found")
if email not in activity["participants"]:
raise HTTPException(
status_code=400, detail="Not registered for this activity")
result = activities_collection.update_one(
{"_id": activity_name},
{"$pull": {"participants": email}}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Failed to update activity")
return {"message": f"Unregistered {email} from {activity_name}"}
@@ -1,0 +1,51 @@
"""
Authentication endpoints for the High School Management System API
"""
from fastapi import APIRouter, HTTPException
from typing import Dict, Any
import hashlib
from ..database import teachers_collection
router = APIRouter(
prefix="/auth",
tags=["auth"]
)
def hash_password(password):
"""Hash password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
@router.post("/login")
def login(username: str, password: str) -> Dict[str, Any]:
"""Login a teacher account"""
hashed_password = hash_password(password)
teacher = teachers_collection.find_one({"_id": username})
if not teacher or teacher["password"] != hashed_password:
raise HTTPException(status_code=401, detail="Invalid username or password")
return {
"username": teacher["username"],
"display_name": teacher["display_name"],
"role": teacher["role"]
}
@router.get("/check-session")
def check_session(username: str) -> Dict[str, Any]:
"""Check if a session is valid by username"""
teacher = teachers_collection.find_one({"_id": username})
if not teacher:
raise HTTPException(status_code=404, detail="Teacher not found")
return {
"username": teacher["username"],
"display_name": teacher["display_name"],
"role": teacher["role"]
}