Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*/venv/
*/node_modules/
__pycache__/
*.pyc
53 changes: 53 additions & 0 deletions README-EVAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Snappet Challenge by Evan Sahit

This is project is my contribution for the Snappet Challenge where I built a dashboard to visualize the given dataset of exercises completed by students.
I've also included my notes for this project outlining my thought process (.pdf file at root of the project), in case anyone is interested.

## Steps to run get the project running locally

### Backend

The backend is a FastAPI (Python) project, so an installation of Python is required. This project is using Python 3.13. Follow the following steps:

0. Follow the necessary steps to install Python 3.13.

1. Navigate into the `/backend` folder. From the root of the project:

- `$ cd backend`

2. Create a Python virtual environment called `venv` using the `venv` package:

- `$ python -m venv venv`

3. Activate the newly created virtual environment:

- On MacOS/ Linux: `$ source venv/bin/activate`
- On Windows: `$ venv\Scripts\activate.bat`
- If the virtual environment has been properly activated, you will see `(venv)` inside of your terminal at the start of the line, e.g. `$ (venv) evan@Evan-PC:~/dev/snappet/snappet-challenge/SnappetChallenge/backend$`
- You can also double check which Python interpreter is being used by running `$ which python` on MacOS and Linux or `$ where python` on Windows. This should output the path to the Python interpreter used to create the virtual environment, e.g. `/home/evan/dev/snappet/snappet-challenge/SnappetChallenge/backend/venv/bin/python`.

4. Install the dependencies for FastAPI and Pandas into the virtual environment:

- `$ pip install -r requirements.txt`

5. Run the backend FastAPI server:

- `$ fastapi dev app/main.py`

### Frontend

The frontend is a React project scaffolded with Vite and using TypeScript and TailwindCSS. Follow the following steps.

0. Follow the necessary steps to install Node.js

1. Navigate into the `/frontend` folder. From the root of the project:

- `$ cd frontend/`

2. Install the dependencies for React and Rechart:

- `$ npm i`

3. Run the React development server via Vite:

- `$ npm run dev`
Empty file added backend/app/__init__.py
Empty file.
Empty file added backend/app/api/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions backend/app/api/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pandas as pd
from fastapi import Query


def load_data_dep():
df = pd.read_csv("data/work.csv")
# parse datetime values
df["SubmitDateTime"] = pd.to_datetime(df["SubmitDateTime"], format="ISO8601")

return df


def shared_datetime_params_dep(
date_str: str = Query(..., alias="date"),
start_time_str: str = Query(..., alias="start-time"),
end_time_str: str = Query(..., alias="end-time"),
):
return {
"date_str": date_str,
"start_time_str": start_time_str,
"end_time_str": end_time_str,
}
8 changes: 8 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import APIRouter

from app.api.routes import classes, students, subjects

router = APIRouter()
router.include_router(classes.router, tags=["Classes"])
router.include_router(subjects.router, tags=["Subjects"])
router.include_router(students.router, tags=["Students"])
Empty file.
61 changes: 61 additions & 0 deletions backend/app/api/routes/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Annotated

import pandas as pd
from fastapi import APIRouter, Depends, status

from app.api.dependencies import load_data_dep, shared_datetime_params_dep
from app.schemas.classes import ProgressSumForColumn
from app.service.class_service import ClassService

router = APIRouter(prefix="/classes")


@router.get(
"/progress_sum_per_subject",
response_model=list[ProgressSumForColumn],
status_code=status.HTTP_200_OK,
)
async def get_progress_sum_per_subject(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return ClassService.get_progress_sum_per_subject(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)


@router.get(
"/progress_sum_per_learning_objective",
response_model=list[ProgressSumForColumn],
status_code=status.HTTP_200_OK,
)
async def get_progress_sum_per_learning_objective(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return ClassService.get_progress_sum_per_learning_objective(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)


@router.get(
"/progress_sum_per_student",
response_model=list[ProgressSumForColumn],
status_code=status.HTTP_200_OK,
)
async def get_progress_sum_per_student(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return ClassService.get_progress_sum_per_student(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)
29 changes: 29 additions & 0 deletions backend/app/api/routes/students.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Annotated

import pandas as pd
from fastapi import APIRouter, Depends, status

from app.api.dependencies import load_data_dep, shared_datetime_params_dep
from app.schemas.students import StudentProgressForSubject
from app.service.student_service import StudentService

router = APIRouter(prefix="/students")


@router.get(
"/progress-per-subject",
response_model=list[StudentProgressForSubject],
status_code=status.HTTP_200_OK,
)
async def get_progress_per_subject(
user_id: str,
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return StudentService.get_progress_per_subject(
user_id,
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)
83 changes: 83 additions & 0 deletions backend/app/api/routes/subjects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from typing import Annotated

import pandas as pd
from fastapi import APIRouter, Depends, status

from app.api.dependencies import load_data_dep, shared_datetime_params_dep
from app.schemas.subjects import (
ExerciseCountForDomain,
ExerciseCountForSubject,
PerformanceForSubject,
StudentCountForSubject,
)
from app.service.subject_service import SubjectService

router = APIRouter(prefix="/subjects")


@router.get(
"/student-count-per-subject",
response_model=list[StudentCountForSubject],
status_code=status.HTTP_200_OK,
)
async def get_student_count_per_subject(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return SubjectService.get_student_count_per_subject(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)


@router.get(
"/exercise-count-per-subject",
response_model=list[ExerciseCountForSubject],
status_code=status.HTTP_200_OK,
)
async def get_exercise_count_per_subject(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return SubjectService.get_exercise_count_per_subject(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)


@router.get(
"/exercise-count-per-domain",
response_model=list[ExerciseCountForDomain],
status_code=status.HTTP_200_OK,
)
async def get_exercise_count_per_domain(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return SubjectService.get_exercise_count_per_domain(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)


@router.get(
"/performance-per-subject",
response_model=list[PerformanceForSubject],
status_code=status.HTTP_200_OK,
)
async def get_performance_per_subject(
shared_dt_params: Annotated[dict, Depends(shared_datetime_params_dep)],
work_df: Annotated[pd.DataFrame, Depends(load_data_dep)],
):
return SubjectService.get_performance_per_subject(
shared_dt_params["date_str"],
shared_dt_params["start_time_str"],
shared_dt_params["end_time_str"],
work_df,
)
51 changes: 51 additions & 0 deletions backend/app/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from datetime import datetime

import pandas as pd
from fastapi import HTTPException, status

from app.schemas.classes import ProgressSumForColumn


def filter_df_by_timerange(
df: pd.DataFrame,
date_str: str,
start_time_str: str,
end_time_str: str,
column="SubmitDateTime",
):
start_time = f"{date_str}T{start_time_str}"
end_time = f"{date_str}T{end_time_str}"
start_dt = datetime.fromisoformat(
start_time,
)
end_dt = datetime.fromisoformat(end_time)

if start_dt > end_dt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Opgegeven start tijd is na de eind tijd",
)

return df.loc[(df[column] >= start_dt) & (df[column] <= end_dt)]


def filter_df_by_user_id(df: pd.DataFrame, user_id: str):
res = df[df["UserId"] == int(user_id)]

return res


def get_progress_groupedby_column(
df: pd.DataFrame, column: str
) -> list[ProgressSumForColumn]:
progress_counts_df = df.groupby(column, as_index=False)["Progress"].sum()

progress_column_objs = []
for _, row in progress_counts_df.iterrows():
progress_column_objs.append(
ProgressSumForColumn(
column=str(row[column]), progress_sum=int(row["Progress"])
)
)

return progress_column_objs
6 changes: 6 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Settings:
API_V1_STR = "/api/v1"

PROJECT_TITLE = "Snappet Challenge API"
PROJECT_DESCRIPTION = "The Snappet Challenge is a technical assessment for software development candidates"
CONTACT = {"name": "Evan Sahit", "email": "[email protected]"}
23 changes: 23 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api.main import router
from app.config import Settings

app = FastAPI(
title=Settings.PROJECT_TITLE,
description=Settings.PROJECT_DESCRIPTION,
contact=Settings.CONTACT,
)

app.include_router(router, prefix=Settings.API_V1_STR)

origins = ["http://localhost:5173"]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
6 changes: 6 additions & 0 deletions backend/app/schemas/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class ProgressSumForColumn(BaseModel):
column: str
progress_sum: int
7 changes: 7 additions & 0 deletions backend/app/schemas/students.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel


class StudentProgressForSubject(BaseModel):
user_id: str
subject: str
progress_sum: int
22 changes: 22 additions & 0 deletions backend/app/schemas/subjects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pydantic import BaseModel


class StudentCountForSubject(BaseModel):
subject: str
student_count: int


class ExerciseCountForSubject(BaseModel):
subject: str
exercise_count: int


class ExerciseCountForDomain(BaseModel):
domain: str
exercise_count: int


class PerformanceForSubject(BaseModel):
subject: str
correct_count: int
error_count: int
Empty file added backend/app/service/__init__.py
Empty file.
Loading