diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..87ca42d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Python Debugger: FastAPI", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "main:app", + "--reload" + ], + "jinja": true, + "cwd": "${workspaceFolder}/src" + } + ] +} \ No newline at end of file diff --git a/data/max/reservations.json b/data/max/reservations.json new file mode 100644 index 0000000..a62ccbe --- /dev/null +++ b/data/max/reservations.json @@ -0,0 +1,8 @@ +{ + "reservations": [ + { + "item_id": "77b3c3a0-303b-482f-a1e0-67c29030ec69", + "name": "Max" + } + ] +} \ No newline at end of file diff --git a/src/main.py b/src/main.py index 0876a45..9c3e6cd 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,14 @@ import uuid -from fastapi import FastAPI, Request, Response +from fastapi import Depends, FastAPI, Request, Response from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from wishlist import read_all_wishlists +from wishlist import ( + read_all_wishlists, + ReservationRequest, + add_reservation, + remove_reservation, + ItemReservation, +) WISHLISTS_DIR = "../data" @@ -10,13 +16,37 @@ app = FastAPI() templates = Jinja2Templates(directory="templates") -@app.get("/{id}/view", response_class=HTMLResponse) -async def read_item(request: Request, id: uuid.UUID): +wishlist_cache = {} + + +@app.post("/reservation/update") +async def update_reservation( + reservation_request: ReservationRequest = Depends( + ReservationRequest.parse_reservation_request + ), +): + global wishlist_cache + wishlist_dir: str = wishlist_cache[reservation_request.wishlist_id].directory + if reservation_request.reserved: + new_reservation = ItemReservation() + new_reservation.name = reservation_request.name + new_reservation.item_id = str(reservation_request.item_id) + add_reservation(wishlist_dir, new_reservation) + else: + remove_reservation(wishlist_dir, reservation_request.item_id) + + +@app.get("/{wishlist_id}/view", response_class=HTMLResponse) +async def show_wishlist(request: Request, wishlist_id: uuid.UUID): + global wishlist_cache wishlists = read_all_wishlists(WISHLISTS_DIR) - - if str(id) not in wishlists.keys(): + wishlist_cache = wishlists + + if wishlist_id not in wishlists.keys(): return Response(status_code=404) - + return templates.TemplateResponse( - request=request, name="wishlist_view.html", context={"wishlist": wishlists[str(id)]} - ) \ No newline at end of file + request=request, + name="wishlist_view.html", + context={"wishlist": wishlists[wishlist_id]}, + ) diff --git a/src/templates/components/wishlist_item.html b/src/templates/components/wishlist_item.html new file mode 100644 index 0000000..88d3c1d --- /dev/null +++ b/src/templates/components/wishlist_item.html @@ -0,0 +1,25 @@ + +
  • +
    +

    {{ item.name }}{% if item.reservation %} (Reserved){% endif %}

    +

    {{ item.description }}

    +

    €{{ "%.2f"|format(item.price) }}

    + Shop Link + + +
    + + + + {% if item.reserved %} + + + {% else %} + + + {% endif %} +
    +
    + {{ item.name }} +
  • +
    diff --git a/src/templates/wishlist_view.html b/src/templates/wishlist_view.html index 6ed3534..bb37f8c 100644 --- a/src/templates/wishlist_view.html +++ b/src/templates/wishlist_view.html @@ -132,52 +132,16 @@

    {{ wishlist.config.title }}

    Show Reserved Items
    diff --git a/src/wishlist/__init__.py b/src/wishlist/__init__.py index 8ebe8f8..33bd8a4 100644 --- a/src/wishlist/__init__.py +++ b/src/wishlist/__init__.py @@ -1,2 +1,8 @@ -from .models import Wishlist, WishlistConfig, WishlistItem -from .storage import read_all_wishlists \ No newline at end of file +from .models import ( + Wishlist, + WishlistConfig, + WishlistItem, + ReservationRequest, + ItemReservation, +) +from .storage import read_all_wishlists, add_reservation, remove_reservation diff --git a/src/wishlist/models.py b/src/wishlist/models.py index b49ff0e..202f154 100644 --- a/src/wishlist/models.py +++ b/src/wishlist/models.py @@ -1,33 +1,54 @@ import uuid from datetime import date +from fastapi import Form +from pydantic import BaseModel + + +class ReservationRequest(BaseModel): + wishlist_id: uuid.UUID + item_id: uuid.UUID + reserved: bool + name: str + + async def parse_reservation_request( + wishlist_id: uuid.UUID = Form(...), + item_id: uuid.UUID = Form(...), + reserved: bool = Form(...), + name: str = Form(...), + ) -> "ReservationRequest": + return ReservationRequest( + wishlist_id=wishlist_id, item_id=item_id, reserved=reserved, name=name + ) + class ItemReservation: def __init__(self) -> None: + self.item_id: uuid.UUID = uuid.uuid4() self.name: str = "" class WishlistItem: def __init__(self) -> None: - self.id: uuid = uuid.uuid4() + self.id: uuid.UUID = uuid.uuid4() self.name: str = "" self.description: str = "" self.shop: str = "" self.image: str = "" self.price: float = 0 self.reservation: ItemReservation | None = None - + @property def is_reserved(self) -> bool: return self.reservation is not None class WishlistConfig: def __init__(self) -> None: - self.id: uuid = uuid.uuid4() + self.id: uuid.UUID = uuid.uuid4() self.title: str = "" - self.deadlines: dict[str, date] = {} + self.deadlines: dict[uuid.UUID, date] = {} self.deadline_offset_days: int = 0 class Wishlist: def __init__(self) -> None: self.directory: str = "" - self.items: dict[str, WishlistItem] = {} # Mapping Item ID to item - self.config: WishlistConfig = None \ No newline at end of file + self.items: dict[uuid.UUID, WishlistItem] = {} # Mapping Item ID to item + self.config: WishlistConfig = None diff --git a/src/wishlist/storage.py b/src/wishlist/storage.py index 4a17c54..ad51f9d 100644 --- a/src/wishlist/storage.py +++ b/src/wishlist/storage.py @@ -1,60 +1,125 @@ -from .models import Wishlist, WishlistConfig, WishlistItem +from .models import Wishlist, WishlistConfig, WishlistItem, ItemReservation from datetime import date import uuid import json import csv import os +CONFIG_NAME = "config.json" +WISHLIST_NAME = "wishlist.csv" +RESERVATIONS_NAME = "reservations.json" + + def read_wishlist_config(wishlist_dir: str) -> WishlistConfig: - path: str = f"{wishlist_dir}/config.json" + path: str = f"{wishlist_dir}/{CONFIG_NAME}" with open(path, "r", encoding="UTF-8") as fp: json_config = json.load(fp) - + config = WishlistConfig() config.id = uuid.UUID(json_config["id"]) config.title = json_config["title"] config.deadline_offset_days = json_config["deadlineOffsetDays"] - + for label, timestamp in json_config["deadlines"].items(): config.deadlines[label] = date.fromisoformat(timestamp) return config -def read_wishlist_items(wishlist_dir: str) -> dict[str, WishlistItem]: - items: dict[str, WishlistItem] = {} - path: str = f"{wishlist_dir}/wishlist.csv" - with open(path, "r", encoding = "UTF-8") as fp: - csvreader = csv.DictReader(fp) - - for row in csvreader: - item: WishlistItem = WishlistItem() - item.id = uuid.UUID(row.get("id")) - item.name = row.get("name") - item.description = row.get("description") - item.shop = row.get("shop") - item.image = row.get("image") - item.price = float(row.get("price")) - - items[str(item.id)] = item - + +def read_wishlist_items(wishlist_dir: str) -> dict[uuid.UUID, WishlistItem]: + reservations = read_all_reservations(wishlist_dir) + + items: dict[uuid.UUID, WishlistItem] = {} + path: str = f"{wishlist_dir}/{WISHLIST_NAME}" + with open(path, "r", encoding="UTF-8") as fp: + csvreader = csv.DictReader(fp) + + for row in csvreader: + item: WishlistItem = WishlistItem() + item.id = uuid.UUID(row.get("id")) + item.name = row.get("name") + item.description = row.get("description") + item.shop = row.get("shop") + item.image = row.get("image") + item.price = float(row.get("price")) + + if item.id in reservations.keys(): + item.reservation = reservations[item.id] + + items[item.id] = item + return items - + def read_wishlist(wishlist_dir: str) -> Wishlist: wishlist = Wishlist() wishlist.directory = wishlist_dir - + wishlist.items = read_wishlist_items(wishlist.directory) wishlist.config = read_wishlist_config(wishlist.directory) - + return wishlist -def read_all_wishlists(directory: str) -> dict[str, Wishlist]: + +def read_all_wishlists(directory: str) -> dict[uuid.UUID, Wishlist]: parent_dir, wishlist_dirs, _ = list(os.walk(directory))[0] - - wishlists: dict[str, Wishlist] = {} + + wishlists: dict[uuid.UUID, Wishlist] = {} for dir in wishlist_dirs: wishlist: Wishlist = read_wishlist(f"{parent_dir}/{dir}") - wishlists[str(wishlist.config.id)] = wishlist - - return wishlists \ No newline at end of file + wishlists[wishlist.config.id] = wishlist + + return wishlists + + +def read_all_reservations(wishlist_dir: str) -> dict[uuid.UUID, ItemReservation]: + path: str = f"{wishlist_dir}/{RESERVATIONS_NAME}" + if not os.path.exists(path): + return {} + + reservations_json = [] + with open(path, "r", encoding="UTF-8") as fp: + reservations_json = json.load(fp)["reservations"] + + reservations = {} + for r in reservations_json: + reservation = ItemReservation() + reservation.item_id = uuid.UUID(r["item_id"]) + reservation.name = r["name"] + + reservations[reservation.item_id] = reservation + + return reservations + + +def write_all_reservations( + wishlist_dir: str, reservations: dict[uuid.UUID, ItemReservation] +): + path: str = f"{wishlist_dir}/{RESERVATIONS_NAME}" + + reservations_json = [] + for r in reservations.values(): + reservations_json.append({"item_id": r.item_id, "name": r.name}) + + with open(path, "w", encoding="UTF-8") as fp: + json.dump({"reservations": reservations_json}, fp) + + +def add_reservation(wishlist_dir: str, reservation: ItemReservation): + all_reservations = read_all_reservations(wishlist_dir) + if reservation.item_id in all_reservations.keys(): + raise Exception(f"Item [{reservation.item_id}] already reserved.") + + all_reservations[reservation.item_id] = reservation + + write_all_reservations(wishlist_dir, all_reservations) + + +def remove_reservation(wishlist_dir: str, item_id: uuid.UUID): + all_reservations = read_all_reservations(wishlist_dir) + if item_id not in all_reservations.keys(): + return + + del all_reservations[item_id] + + write_all_reservations(wishlist_dir, all_reservations)