From 280a90c696177ef60304ac4da0c3b667bcce0bb4 Mon Sep 17 00:00:00 2001 From: George Kaye Date: Tue, 20 Feb 2024 23:33:01 +0000 Subject: [PATCH 1/2] [impl] Add settings endpoints and database stuff --- api/src/cookiebreaks/api/main.py | 6 +++- api/src/cookiebreaks/api/routers/data.py | 32 ++++++++++++++++++++ api/src/cookiebreaks/api/routers/settings.py | 27 +++++++++++++++++ api/src/cookiebreaks/core/database.py | 31 +++++++++++++++++++ api/src/cookiebreaks/core/structs.py | 9 ++++++ db/init.sql | 12 +++++++- 6 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 api/src/cookiebreaks/api/routers/data.py create mode 100644 api/src/cookiebreaks/api/routers/settings.py diff --git a/api/src/cookiebreaks/api/main.py b/api/src/cookiebreaks/api/main.py index 2f64521..9f22a75 100644 --- a/api/src/cookiebreaks/api/main.py +++ b/api/src/cookiebreaks/api/main.py @@ -1,10 +1,12 @@ from fastapi import FastAPI -from cookiebreaks.api.routers import users, breaks, claims, debug +from cookiebreaks.api.routers import data, users, breaks, claims, debug, settings from cookiebreaks.core.env import get_env_path, get_env_variable, load_envs tags_metadata = [ {"name": "users", "description": "Authenticate users"}, + {"name": "data", "description": "Get cookie break data"}, + {"name": "settings", "description": "View and modify cookie break settings"}, {"name": "breaks", "description": "Operations for interacting with cookie breaks"}, { "name": "claims", @@ -30,6 +32,8 @@ ) app.include_router(users.router) +app.include_router(data.router) +app.include_router(settings.router) app.include_router(breaks.router) app.include_router(claims.router) app.include_router(debug.router) diff --git a/api/src/cookiebreaks/api/routers/data.py b/api/src/cookiebreaks/api/routers/data.py new file mode 100644 index 0000000..aee43f0 --- /dev/null +++ b/api/src/cookiebreaks/api/routers/data.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import Annotated +from cookiebreaks.api.routers.users import get_current_user +from cookiebreaks.api.routers.utils import ( + BreakExternal, + ClaimExternal, + get_breaks, + get_claims, +) +from cookiebreaks.core.database import select_settings +from cookiebreaks.core.structs import Settings, User +from fastapi import APIRouter, Depends + + +router = APIRouter(prefix="/data", tags=["data"]) + + +@dataclass +class Data: + settings: Settings + breaks: list[BreakExternal] + claims: list[ClaimExternal] + + +@router.get("", response_model=Data, summary="Get all data") +def get_settings( + current_user: Annotated[User, Depends(get_current_user)], +): + settings = select_settings() + breaks = get_breaks(current_user=current_user) + claims = get_claims(current_user=current_user) + return Data(settings, breaks, claims) diff --git a/api/src/cookiebreaks/api/routers/settings.py b/api/src/cookiebreaks/api/routers/settings.py new file mode 100644 index 0000000..e3ab46e --- /dev/null +++ b/api/src/cookiebreaks/api/routers/settings.py @@ -0,0 +1,27 @@ +from datetime import time +from decimal import Decimal +from typing import Annotated, Optional +from cookiebreaks.api.routers.users import is_admin +from cookiebreaks.core.database import select_settings +from cookiebreaks.core.structs import Settings, User +from fastapi import APIRouter, Depends + + +router = APIRouter(prefix="/settings", tags=["settings"]) + + +@router.get("", response_model=Settings, summary="Get settings") +def get_settings(): + settings = select_settings() + return settings + + +@router.post("", response_model=Settings, summary="Update settings") +def post_settings( + user: Annotated[User, Depends(is_admin)], + day: int, + time: time, + budget: Decimal, + location: str, +): + set_settings(day, time, budget, location) diff --git a/api/src/cookiebreaks/core/database.py b/api/src/cookiebreaks/core/database.py index dc5618e..013b50e 100644 --- a/api/src/cookiebreaks/core/database.py +++ b/api/src/cookiebreaks/core/database.py @@ -1,3 +1,4 @@ +from datetime import time from decimal import Decimal from unittest.util import strclass import arrow @@ -12,6 +13,7 @@ Claim, ClaimFilters, Arrow, + Settings, User, ) @@ -32,6 +34,35 @@ def disconnect(conn: Any, cur: Any) -> None: cur.close() +def select_settings() -> Settings: + (conn, cur) = connect() + statement = """ + SELECT day, time, location, budget + FROM Settings + """ + cur.execute(statement) + (day, time, location, budget) = cur.fetchall()[0] + conn.close() + return Settings(day, time, location, budget) + + +def set_settings(day: int, time: time, location: str, budget: Decimal): + (conn, cur) = connect() + statement = """ + UPDATE Settings + SET + day = %(day)s, + time = %(time)s, + location = %(location)s, + budget = %(budget)s + """ + cur.execute( + statement, {"day": day, "time": time, "location": location, "budget": budget} + ) + conn.commit() + conn.close() + + def get_user(username: str) -> Optional[User]: (conn, cur) = connect() statement = """ diff --git a/api/src/cookiebreaks/core/structs.py b/api/src/cookiebreaks/core/structs.py index 59e5be0..0a6e842 100644 --- a/api/src/cookiebreaks/core/structs.py +++ b/api/src/cookiebreaks/core/structs.py @@ -2,6 +2,15 @@ from decimal import Decimal from typing import Literal, Optional from arrow import Arrow +from datetime import time + + +@dataclass +class Settings: + day: int + time: time + location: str + budget: Decimal @dataclass diff --git a/db/init.sql b/db/init.sql index 1ca0751..f29f7a9 100644 --- a/db/init.sql +++ b/db/init.sql @@ -1,3 +1,13 @@ +CREATE TABLE Settings ( + day INT NOT NULL, + time TIME NOT NULL, + location TEXT NOT NULL, + budget DECIMAL, + CONSTRAINT max_day CHECK (day between 0 and 6) +); +INSERT INTO Settings (day, time, location, budget) VALUES ( + 2, '14:30', 'LG06a', 15.0 +); CREATE TABLE Person ( user_name TEXT PRIMARY KEY, admin BOOLEAN NOT NULL, @@ -26,7 +36,7 @@ CREATE TABLE ClaimItem ( FOREIGN KEY(claim_id) REFERENCES Claim(claim_id) ON DELETE CASCADE, FOREIGN KEY(break_id) REFERENCES Break(break_id) ON DELETE CASCADE ); -INSERT INTO PERSON (user_name, hashed_password, admin, email) VALUES ( +INSERT INTO Person (user_name, hashed_password, admin, email) VALUES ( 'admin', '$2b$12$nstgZNzQWP5vWThNoxG.pOuTrqpgvxsoztJOZXE2gSVx8dD8OySkW', 'true', From 5b5dca3653130288efa2dd54b60133b35bef2dd1 Mon Sep 17 00:00:00 2001 From: George Kaye Date: Tue, 20 Feb 2024 23:33:55 +0000 Subject: [PATCH 2/2] [feat] Add settings page --- client/src/app/api.ts | 56 ++++++++++++++++-- client/src/app/bar.tsx | 34 +++++++++-- client/src/app/page.tsx | 120 +++++++++++++++++++++++--------------- client/src/app/structs.ts | 47 +++++++++++++++ 4 files changed, 202 insertions(+), 55 deletions(-) diff --git a/client/src/app/api.ts b/client/src/app/api.ts index 3dfef1d..3b2530f 100644 --- a/client/src/app/api.ts +++ b/client/src/app/api.ts @@ -1,10 +1,26 @@ import axios from "axios" -import { Claim, CookieBreak, UpdateFn, User } from "./structs" +import { + Claim, + CookieBreak, + Day, + Settings, + UpdateFn, + User, + dayNumberToDay, + dayToDayNumber, +} from "./structs" import { Data, SetState } from "./page" const dateOrUndefined = (datetime: string | undefined) => datetime ? new Date(datetime) : undefined +const responseToSettings = (set: any) => ({ + day: dayNumberToDay(set.day), + time: new Date(`1970-01-01T${set.time}`), + location: set.location, + budget: Number.parseFloat(set.budget), +}) + const responseToBreak = (b: any) => ({ id: b.id, host: b.host, @@ -71,11 +87,13 @@ export const login = async ( ) setBreaks(breakObjects) setClaims(claimObjects) + setTimeout(() => setLoading(false), 1) + return 0 } catch (err) { console.log(err) setStatus("Could not log in...") - } finally { setTimeout(() => setLoading(false), 1) + return 1 } } } @@ -84,16 +102,46 @@ export const getData = async ( user: User | undefined, setBreaks: SetState, setClaims: SetState, + setSettings: SetState, setLoadingData: SetState ) => { setLoadingData(true) - let breaks = await getBreaks(user) - let claims = await getClaims(user, breaks) + let endpoint = `/api/data` + let config = { + headers: getHeaders(user), + } + let response = await axios.get(endpoint, config) + let data = response.data + let settings = responseToSettings(data.settings) + let breaks = data.breaks.map(responseToBreak) + let claims = data.claims.map(responseToClaim) + setSettings(settings) setBreaks(breaks) setClaims(claims) setTimeout(() => setLoadingData(false)) } +export const postSettings = async ( + user: User | undefined, + day: Day, + time: Date, + location: string, + budget: number +) => { + let endpoint = `/api/settings` + let config = { + params: { + day: dayToDayNumber(day), + time: time.toTimeString().substring(0, 5), + location: location, + budget: budget, + }, + headers: getHeaders(user), + } + let response = await axios.get(endpoint, config) + return response.status +} + export const getBreaks = async (user: User | undefined) => { let endpoint = `/api/breaks` let config = { diff --git a/client/src/app/bar.tsx b/client/src/app/bar.tsx index a657047..e4f9d5e 100644 --- a/client/src/app/bar.tsx +++ b/client/src/app/bar.tsx @@ -1,9 +1,9 @@ import React, { Dispatch, SetStateAction, useState } from "react" -import { User, CookieBreak, Claim } from "./structs" +import { User, CookieBreak, Claim, Settings } from "./structs" import Loader from "./loader" import { getData, login } from "./api" import { LoginModal, LogoutModal } from "./modals/login" -import { Data, SetState } from "./page" +import { Data, Mode, SetState } from "./page" const InputBox = (props: { type: string @@ -30,6 +30,8 @@ const LoginButton = (props: { setClaims: SetState setLoadingLogin: SetState user: User | undefined + mode: Mode + setMode: SetState }) => { const [isActive, setActive] = useState(false) const [userText, setUserText] = useState("") @@ -54,9 +56,13 @@ const LoginButton = (props: { <>
+ props.mode === Mode.Main + ? props.setMode(Mode.Admin) + : props.setMode(Mode.Main) + } > - {!props.user ? "Login" : props.user.user} + {!props.user ? "Login" : "Settings"}
{!props.user ? ( setClaims: SetState user: User | undefined + mode: Mode + setMode: SetState }) => { const [isLoadingLogin, setLoadingLogin] = useState(false) return ( @@ -98,6 +106,8 @@ const LoginRegion = (props: { setClaims={props.setClaims} setUser={props.setUser} setLoadingLogin={setLoadingLogin} + mode={props.mode} + setMode={props.setMode} /> )} @@ -108,6 +118,7 @@ const RefreshButton = (props: { user: User | undefined setBreaks: SetState setClaims: SetState + setSettings: SetState setLoadingData: SetState }) => { const onClickRefresh = (e: React.MouseEvent) => @@ -115,6 +126,7 @@ const RefreshButton = (props: { props.user, props.setBreaks, props.setClaims, + props.setSettings, props.setLoadingData ) return @@ -124,15 +136,19 @@ const RightButtons = (props: { setUser: SetState setBreaks: SetState setClaims: SetState + setSettings: SetState setLoadingData: SetState user: User | undefined + mode: Mode + setMode: SetState }) => { return ( -
+
) @@ -149,8 +167,11 @@ export const TopBar = (props: { setUser: SetState setBreaks: SetState setClaims: SetState + setSettings: SetState setLoadingData: SetState user: User | undefined + mode: Mode + setMode: SetState }) => { return (
@@ -159,8 +180,11 @@ export const TopBar = (props: { user={props.user} setBreaks={props.setBreaks} setClaims={props.setClaims} + setSettings={props.setSettings} setLoadingData={props.setLoadingData} setUser={props.setUser} + mode={props.mode} + setMode={props.setMode} />
) diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx index 71d6362..6aaa071 100644 --- a/client/src/app/page.tsx +++ b/client/src/app/page.tsx @@ -1,7 +1,7 @@ "use client" import React, { useEffect, useState } from "react" -import { Claim, CookieBreak, User, replaceItems } from "./structs" +import { Claim, CookieBreak, Settings, User, replaceItems } from "./structs" import { getData, submitClaim } from "./api" import { TopBar } from "./bar" import { Manrope } from "next/font/google" @@ -9,6 +9,7 @@ import { UpcomingBreaksCards } from "./cards/upcoming" import { AwaitingReimbursementCards } from "./cards/reimbursement" import { AwaitingClaimCards } from "./cards/awaiting" import { AwaitingCompletionCards } from "./cards/completion" +import { AdminPage } from "./admin" export type SetState = React.Dispatch> @@ -24,14 +25,21 @@ export interface Data { claims: Claim[] } +export enum Mode { + Main, + Admin, +} + const Home = () => { + const [mode, setMode] = useState(Mode.Main) const [user, setUser] = useState(undefined) // All data const [claims, setClaims] = useState([]) const [breaks, setBreaks] = useState([]) + const [settings, setSettings] = useState(undefined) const [isLoadingData, setLoadingData] = useState(false) useEffect(() => { - getData(user, setBreaks, setClaims, setLoadingData) + getData(user, setBreaks, setClaims, setSettings, setLoadingData) }, []) useEffect(() => { console.log(claims) @@ -82,59 +90,79 @@ const Home = () => { user={user} setBreaks={setBreaks} setClaims={setClaims} + setSettings={setSettings} setLoadingData={setLoadingData} + mode={mode} + setMode={setMode} />
-
- The cookie break is the school's longest running social - event: every week a different host buys some biscuits up - to the amount of £15 and shares them with everyone else. - Thanks to the gracious funding of Research Committee, - they get reimbursed for their troubles! -
-
- - {!user?.admin ? ( - "" - ) : ( - <> - +
+ The cookie break is the school's longest running + social event: every week a different host buys + some biscuits up to the amount of £15 and shares + them with everyone else. Thanks to the gracious + funding of Research Committee, they get + reimbursed for their troubles! +
+
+ - - - - )} -
-
- This tool is in{" "} - beta! - Please report any bugs or suggestions on{" "} - - GitHub - - . -
+ {!user?.admin ? ( + "" + ) : ( + <> + + + + + )} +
+
+ This tool is in{" "} + + beta + + ! Please report any bugs or suggestions on{" "} + + GitHub + + . +
+ + )}
diff --git a/client/src/app/structs.ts b/client/src/app/structs.ts index 3ecd027..05e0190 100644 --- a/client/src/app/structs.ts +++ b/client/src/app/structs.ts @@ -27,6 +27,53 @@ export interface Claim { reimbursed?: Date } +export enum Day { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +export const dayNumberToDay = (d: number) => + d === 0 + ? Day.Monday + : d === 1 + ? Day.Tuesday + : d === 2 + ? Day.Wednesday + : d === 3 + ? Day.Thursday + : d === 4 + ? Day.Friday + : d === 5 + ? Day.Saturday + : Day.Sunday + +export const dayToDayNumber = (d: Day) => + d === Day.Monday + ? 0 + : d === Day.Tuesday + ? 1 + : d === Day.Wednesday + ? 2 + : d === Day.Thursday + ? 3 + : d === Day.Friday + ? 4 + : d === Day.Saturday + ? 5 + : 6 + +export interface Settings { + day: Day + time: Date + budget: number + location: string +} + export const breakInPast = (cookieBreak: CookieBreak) => dateInPast(cookieBreak.datetime)