├── requirements.txt ├── app ├── models.py ├── routes │ ├── register.py │ ├── status.py │ └── update.py ├── crud2.py ├── crud.py ├── database.py └── main.py ├── Dockerfile ├── templates ├── register.html ├── index.html ├── progress.html └── update.html ├── README.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | pymongo 4 | python-dotenv 5 | hvac 6 | motor 7 | jinja2>=3.0.0 8 | python-multipart 9 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional, Dict 3 | 4 | class Student(BaseModel): 5 | id: str 6 | name: str 7 | progress: Dict[str, bool] = {} 8 | -------------------------------------------------------------------------------- /app/routes/register.py: -------------------------------------------------------------------------------- 1 | 2 | from fastapi import APIRouter 3 | from app.crud import create_student 4 | 5 | 6 | 7 | router = APIRouter() 8 | 9 | @router.post("/register") 10 | async def register(name: str): 11 | student = await create_student(name) 12 | return {"student_id": student.id, "message": f"Welcome, {student.name}!"} 13 | -------------------------------------------------------------------------------- /app/routes/status.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from app.crud import get_student 3 | 4 | router = APIRouter() 5 | 6 | @router.get("/status/{student_id}") 7 | async def status(student_id: str): 8 | student = await get_student(student_id) 9 | if not student: 10 | return {"error": "Student not found"} 11 | return student 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM python:3.10-slim 3 | 4 | # Set workdir 5 | WORKDIR /app 6 | 7 | # Copy project 8 | COPY . . 9 | 10 | # Install dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Expose app port 14 | EXPOSE 8000 15 | 16 | # Start app 17 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] 18 | 19 | -------------------------------------------------------------------------------- /app/routes/update.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from app.crud import update_progress 3 | 4 | 5 | 6 | router = APIRouter() 7 | 8 | @router.post("/update/{student_id}") 9 | async def update(student_id: str, week: str): 10 | student = await update_progress(student_id, week) 11 | return {"message": f"Progress updated for {week}", "student": student} 12 | -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
{{ message }}
8 | {% endif %} 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Total Registered Students: {{ total }}
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/crud2.py: -------------------------------------------------------------------------------- 1 | from app.database import student_collection 2 | from app.models import Student 3 | from uuid import uuid4 4 | 5 | async def create_student(name: str): 6 | student_id = str(uuid4()) 7 | student = Student(id=student_id, name=name) 8 | await student_collection.insert_one(student.dict()) 9 | return student 10 | 11 | async def get_student(student_id: str): 12 | return await student_collection.find_one({"id": student_id}) 13 | 14 | async def update_progress(student_id: str, week: str): 15 | await student_collection.update_one( 16 | {"id": student_id}, 17 | {"$set": {f"progress.{week}": True}} 18 | ) 19 | return await get_student(student_id) 20 | -------------------------------------------------------------------------------- /app/crud.py: -------------------------------------------------------------------------------- 1 | from app.database import student_collection 2 | from app.models import Student 3 | from uuid import uuid4 4 | 5 | async def create_student(name: str): 6 | student_id = str(uuid4()) 7 | student = Student(id=student_id, name=name) 8 | await student_collection.insert_one(student.dict()) 9 | return student 10 | 11 | async def get_student_progress(name: str): 12 | return await student_collection.find_one({"name": name}) 13 | 14 | async def update_student_progress(name: str, week: str, status: str): 15 | result = await student_collection.update_one( 16 | {"name": name}, 17 | {"$set": {f"progress.week{week}": status}} 18 | ) 19 | if result.modified_count == 0: 20 | return None 21 | return await student_collection.find_one({"name" : name}) 22 | 23 | 24 | async def count_students(): 25 | return await student_collection.count_documents({}) 26 | -------------------------------------------------------------------------------- /templates/progress.html: -------------------------------------------------------------------------------- 1 | 2 | 3 || Name | Week | Status |
|---|---|---|
| {{ name }} | 17 |Week {{ item.week | replace ("week", "") }} | 18 |{{ item.status }} | 19 |
No progress found for {{ name }}
24 | {% endif %} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /templates/update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |{{ message }}
8 | {% endif %} 9 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hvac 3 | import motor.motor_asyncio 4 | 5 | # Get Vault details from environment 6 | vault_addr = os.getenv("VAULT_ADDR") 7 | role_id = os.getenv("VAULT_ROLE_ID") 8 | secret_id = os.getenv("VAULT_SECRET_ID") 9 | 10 | if not all([vault_addr, role_id, secret_id]): 11 | raise EnvironmentError("Missing one or more Vault environment variables (VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID)") 12 | 13 | # Connect to Vault and login with AppRole 14 | try: 15 | vault_client = hvac.Client(url=vault_addr) 16 | login_response = vault_client.auth.approle.login(role_id=role_id, secret_id=secret_id) 17 | vault_client.token = login_response['auth']['client_token'] 18 | except Exception as e: 19 | raise RuntimeError(f"Vault login failed: {e}") 20 | 21 | # Read MongoDB URI from Vault 22 | try: 23 | read_response = vault_client.secrets.kv.v2.read_secret_version(path="student01") 24 | MONGO_URI = read_response["data"]["data"]["MONGO_URI"] 25 | except Exception as e: 26 | raise RuntimeError(f"Error reading MongoDB URI from Vault: {e}") 27 | 28 | # Connect to MongoDB 29 | client = motor.motor_asyncio.AsyncIOMotorClient(MONGO_URI) 30 | database = client["student_project_tracker"] 31 | student_collection = database.get_collection("students") 32 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, Form 2 | from fastapi.responses import HTMLResponse, RedirectResponse 3 | from fastapi.templating import Jinja2Templates 4 | from app.crud import create_student, get_student_progress, update_student_progress, count_students 5 | 6 | app = FastAPI() 7 | templates = Jinja2Templates(directory="templates") 8 | 9 | @app.get("/", response_class=HTMLResponse) 10 | async def home(request: Request): 11 | total = await count_students() 12 | return templates.TemplateResponse("index.html", {"request": request, "total": total}) 13 | 14 | @app.get("/register", response_class=HTMLResponse) 15 | async def register_form(request: Request): 16 | return templates.TemplateResponse("register.html", {"request": request}) 17 | 18 | @app.post("/register", response_class=HTMLResponse) 19 | async def register_submit(request: Request, name: str = Form(...)): 20 | student = await create_student(name) 21 | return templates.TemplateResponse("register.html", {"request": request, "message": f"Welcome, {student.name}!"}) 22 | 23 | @app.get("/progress", response_class=HTMLResponse) 24 | async def progress_form(request: Request): 25 | return templates.TemplateResponse("progress.html", {"request": request}) 26 | 27 | @app.post("/progress", response_class=HTMLResponse) 28 | async def progress_submit(request: Request, name: str = Form(...)): 29 | student = await get_student_progress(name) 30 | progress = [] 31 | 32 | if student and "progress" in student: 33 | for week, status in student["progress"].items(): 34 | progress.append({"week": week, "status": status}) 35 | return templates.TemplateResponse("progress.html", {"request": request, "progress": progress, "name": name}) 36 | 37 | @app.get("/update", response_class=HTMLResponse) 38 | async def update_form(request: Request): 39 | return templates.TemplateResponse("update.html", {"request": request}) 40 | 41 | @app.post("/update", response_class=HTMLResponse) 42 | async def update_submit( 43 | request: Request, 44 | name: str = Form(...), 45 | week: str = Form(...), 46 | status: str = Form(...) 47 | ): 48 | await update_student_progress(name, week, status) 49 | return templates.TemplateResponse("update.html", {"request": request, "message": "Progress updated successfully!"}) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Student-Project-Tracker Web APP 2 | A simple FastAPI web application for registering students and tracking their weekly progress during the Cloud Native Series. 3 | 4 | ### Key Features: 5 | - Register new students (generates a unique ID). 6 | - Track weekly progress for each student. 7 | - All students use one central MongoDB (hosted on MongoDB Atlas or similar). 8 | - Simple endpoints for registration, status check, and progress update. 9 | 10 | ## 📦 Prerequisites 11 | - Python 3.10+ 12 | - Git 13 | - MongoDB Atlas account (to get your connection string) 14 | 15 | --- 16 | 17 | ## 💻 Local Development Setup 18 | 19 | ### 1. Clone the Repository 20 | ```bash 21 | git clone https://github.com/chisomjude/student-project-tracker.git 22 | cd student-project-tracker 23 | ``` 24 | 25 | ### 2. Create Virtual Environment & Install Dependencies 26 | ```bash 27 | python3 -m venv venv 28 | source venv/bin/activate # On Windows use: venv\Scripts\activate 29 | pip install -r requirements.txt 30 | ``` 31 | 32 | ### 3.Db Conenctions 33 | - navigate to app/main and update vault ip : 34 | 35 | ```export VAULT_ADDR= 36 | export VAULT_TOKEN= 37 | ``` 38 | 39 | ### 4. Run the Application Locally 40 | ```bash 41 | uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload 42 | ``` 43 | Visit `http://vmip:8000` to see your app in action. 44 | 45 | --- 46 | 47 | ## 🐳 Docker Instructions 48 | 49 | ### 1. Build Docker Image 50 | ```bash 51 | docker build -t student-tracker . 52 | ``` 53 | 54 | ### 2. Run Docker Container 55 | ```bash 56 | docker run --env-file .env -p 8000:8000 student-tracker 57 | ``` 58 | 59 | ### 3. Push to Docker Hub 60 | Ensure you're logged in: 61 | ```bash 62 | docker login 63 | ``` 64 | Tag and push your image: 65 | ```bash 66 | docker tag student-tracker your-dockerhub-username/student-tracker 67 | 68 | docker push your-dockerhub-username/student-tracker 69 | ``` 70 | 71 | --- 72 | 73 | ## 📬 API Endpoints 74 | 75 | | Method | Endpoint | Description | 76 | |--------|----------|-------------| 77 | | POST | `/register?name=YourName` | Register new student | 78 | | GET | `/status/{student_id}` | View registration and progress | 79 | | POST | `/update/{student_id}?week=week1` | Update progress by week | 80 | 81 | --- 82 | 83 | ## 🌐 Deploying to Cloud (Optional) 84 | You can deploy the app on platforms like: 85 | - Render 86 | - Railway 87 | - Fly.io 88 | - Azure App Service 89 | - Elastic Beanstalk or more 90 | 91 | 92 | ## 👩🏽💻 Built for the Cloud Native Series by Chisom 93 | This project is used for learning cloud-native tools and Handson-Project. 94 | 95 | Feel free to fork and extend it! 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | --------------------------------------------------------------------------------