Implemented reservations
This commit is contained in:
parent
0d54983850
commit
82bc06817b
8 changed files with 222 additions and 87 deletions
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python Debugger: FastAPI",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "uvicorn",
|
||||||
|
"args": [
|
||||||
|
"main:app",
|
||||||
|
"--reload"
|
||||||
|
],
|
||||||
|
"jinja": true,
|
||||||
|
"cwd": "${workspaceFolder}/src"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
8
data/max/reservations.json
Normal file
8
data/max/reservations.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"reservations": [
|
||||||
|
{
|
||||||
|
"item_id": "77b3c3a0-303b-482f-a1e0-67c29030ec69",
|
||||||
|
"name": "Max"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
48
src/main.py
48
src/main.py
|
@ -1,8 +1,14 @@
|
||||||
import uuid
|
import uuid
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import Depends, FastAPI, Request, Response
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
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"
|
WISHLISTS_DIR = "../data"
|
||||||
|
|
||||||
|
@ -10,13 +16,37 @@ app = FastAPI()
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
@app.get("/{id}/view", response_class=HTMLResponse)
|
wishlist_cache = {}
|
||||||
async def read_item(request: Request, id: uuid.UUID):
|
|
||||||
|
|
||||||
|
@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)
|
wishlists = read_all_wishlists(WISHLISTS_DIR)
|
||||||
|
wishlist_cache = wishlists
|
||||||
if str(id) not in wishlists.keys():
|
|
||||||
|
if wishlist_id not in wishlists.keys():
|
||||||
return Response(status_code=404)
|
return Response(status_code=404)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request, name="wishlist_view.html", context={"wishlist": wishlists[str(id)]}
|
request=request,
|
||||||
)
|
name="wishlist_view.html",
|
||||||
|
context={"wishlist": wishlists[wishlist_id]},
|
||||||
|
)
|
||||||
|
|
25
src/templates/components/wishlist_item.html
Normal file
25
src/templates/components/wishlist_item.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!-- item.html -->
|
||||||
|
<li class="{{ 'reserved-item' if item.reserved else '' }}">
|
||||||
|
<div>
|
||||||
|
<h2>{{ item.name }}{% if item.reservation %} (Reserved){% endif %}</h2>
|
||||||
|
<p>{{ item.description }}</p>
|
||||||
|
<p class="price">€{{ "%.2f"|format(item.price) }}</p>
|
||||||
|
<a href="{{ item.shop }}" target="_blank">Shop Link</a>
|
||||||
|
|
||||||
|
<!-- Reserve or Unreserve Form -->
|
||||||
|
<form method="POST" action="/reservation/update">
|
||||||
|
<input type="hidden" name="wishlist_id" value="{{ wishlist.config.id }}">
|
||||||
|
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||||
|
<input type="hidden" name="reserved" value="{{ 'false' if item.reserved else 'true' }}">
|
||||||
|
{% if item.reserved %}
|
||||||
|
<input type="text" name="name">
|
||||||
|
<button type="submit" class="reserve-button">Unreserve</button>
|
||||||
|
{% else %}
|
||||||
|
<input type="text" name="name" placeholder="Your name" required>
|
||||||
|
<button type="submit" class="reserve-button">Reserve</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<img src="{{ item.image }}" alt="{{ item.name }}">
|
||||||
|
</li>
|
||||||
|
<hr>
|
|
@ -132,52 +132,16 @@
|
||||||
<h1>{{ wishlist.config.title }}</h1>
|
<h1>{{ wishlist.config.title }}</h1>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for item_id, item in wishlist.items.items() if not item.reserved %}
|
{% for item_id, item in wishlist.items.items() if not item.reservation %}
|
||||||
<li>
|
{% include 'components/wishlist_item.html' %}
|
||||||
<div>
|
|
||||||
<h2>{{ item.name }}</h2>
|
|
||||||
<p>{{ item.description }}</p>
|
|
||||||
<p class="price">€{{ "%.2f"|format(item.price) }}</p>
|
|
||||||
<a href="{{ item.shop }}" target="_blank">Shop Link</a>
|
|
||||||
|
|
||||||
<!-- Reserve Form -->
|
|
||||||
<form method="POST" action="/reserve-item">
|
|
||||||
<input type="hidden" name="wishlist_id" value="{{ wishlist.config.id }}">
|
|
||||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
|
||||||
<input type="hidden" name="reserved" value="true">
|
|
||||||
<input type="text" name="reserver_name" placeholder="Your name" required>
|
|
||||||
<button type="submit" class="reserve-button">Reserve</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<img src="{{ item.image }}" alt="{{ item.name }}">
|
|
||||||
</li>
|
|
||||||
<hr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- Collapsible Reserved Items -->
|
<!-- Collapsible Reserved Items -->
|
||||||
<div class="collapsible" onclick="toggleReservedItems()">Show Reserved Items</div>
|
<div class="collapsible" onclick="toggleReservedItems()">Show Reserved Items</div>
|
||||||
<ul id="reserved-items">
|
<ul id="reserved-items">
|
||||||
{% for item_id, item in wishlist.items.items() if item.reserved %}
|
{% for item_id, item in wishlist.items.items() if item.reservation %}
|
||||||
<li class="reserved-item">
|
{% include 'components/wishlist_item.html' %}
|
||||||
<div>
|
|
||||||
<h2>{{ item.name }} (Reserved)</h2>
|
|
||||||
<p>{{ item.description }}</p>
|
|
||||||
<p class="price">€{{ "%.2f"|format(item.price) }}</p>
|
|
||||||
<a href="{{ item.shop }}" target="_blank">Shop Link</a>
|
|
||||||
|
|
||||||
<!-- Unreserve Form -->
|
|
||||||
<form method="POST" action="/reserve-item">
|
|
||||||
<input type="hidden" name="wishlist_id" value="{{ wishlist.config.id }}">
|
|
||||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
|
||||||
<input type="hidden" name="reserved" value="false">
|
|
||||||
<input type="text" name="reserver_name" value="{{ item.reserver_name }}" readonly>
|
|
||||||
<button type="submit" class="reserve-button">Unreserve</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<img src="{{ item.image }}" alt="{{ item.name }}">
|
|
||||||
</li>
|
|
||||||
<hr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
from .models import Wishlist, WishlistConfig, WishlistItem
|
from .models import (
|
||||||
from .storage import read_all_wishlists
|
Wishlist,
|
||||||
|
WishlistConfig,
|
||||||
|
WishlistItem,
|
||||||
|
ReservationRequest,
|
||||||
|
ItemReservation,
|
||||||
|
)
|
||||||
|
from .storage import read_all_wishlists, add_reservation, remove_reservation
|
||||||
|
|
|
@ -1,33 +1,54 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import date
|
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:
|
class ItemReservation:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self.item_id: uuid.UUID = uuid.uuid4()
|
||||||
self.name: str = ""
|
self.name: str = ""
|
||||||
|
|
||||||
class WishlistItem:
|
class WishlistItem:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.id: uuid = uuid.uuid4()
|
self.id: uuid.UUID = uuid.uuid4()
|
||||||
self.name: str = ""
|
self.name: str = ""
|
||||||
self.description: str = ""
|
self.description: str = ""
|
||||||
self.shop: str = ""
|
self.shop: str = ""
|
||||||
self.image: str = ""
|
self.image: str = ""
|
||||||
self.price: float = 0
|
self.price: float = 0
|
||||||
self.reservation: ItemReservation | None = None
|
self.reservation: ItemReservation | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_reserved(self) -> bool:
|
def is_reserved(self) -> bool:
|
||||||
return self.reservation is not None
|
return self.reservation is not None
|
||||||
|
|
||||||
class WishlistConfig:
|
class WishlistConfig:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.id: uuid = uuid.uuid4()
|
self.id: uuid.UUID = uuid.uuid4()
|
||||||
self.title: str = ""
|
self.title: str = ""
|
||||||
self.deadlines: dict[str, date] = {}
|
self.deadlines: dict[uuid.UUID, date] = {}
|
||||||
self.deadline_offset_days: int = 0
|
self.deadline_offset_days: int = 0
|
||||||
|
|
||||||
class Wishlist:
|
class Wishlist:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.directory: str = ""
|
self.directory: str = ""
|
||||||
self.items: dict[str, WishlistItem] = {} # Mapping Item ID to item
|
self.items: dict[uuid.UUID, WishlistItem] = {} # Mapping Item ID to item
|
||||||
self.config: WishlistConfig = None
|
self.config: WishlistConfig = None
|
||||||
|
|
|
@ -1,60 +1,125 @@
|
||||||
from .models import Wishlist, WishlistConfig, WishlistItem
|
from .models import Wishlist, WishlistConfig, WishlistItem, ItemReservation
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
CONFIG_NAME = "config.json"
|
||||||
|
WISHLIST_NAME = "wishlist.csv"
|
||||||
|
RESERVATIONS_NAME = "reservations.json"
|
||||||
|
|
||||||
|
|
||||||
def read_wishlist_config(wishlist_dir: str) -> WishlistConfig:
|
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:
|
with open(path, "r", encoding="UTF-8") as fp:
|
||||||
json_config = json.load(fp)
|
json_config = json.load(fp)
|
||||||
|
|
||||||
config = WishlistConfig()
|
config = WishlistConfig()
|
||||||
config.id = uuid.UUID(json_config["id"])
|
config.id = uuid.UUID(json_config["id"])
|
||||||
config.title = json_config["title"]
|
config.title = json_config["title"]
|
||||||
config.deadline_offset_days = json_config["deadlineOffsetDays"]
|
config.deadline_offset_days = json_config["deadlineOffsetDays"]
|
||||||
|
|
||||||
for label, timestamp in json_config["deadlines"].items():
|
for label, timestamp in json_config["deadlines"].items():
|
||||||
config.deadlines[label] = date.fromisoformat(timestamp)
|
config.deadlines[label] = date.fromisoformat(timestamp)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def read_wishlist_items(wishlist_dir: str) -> dict[str, WishlistItem]:
|
|
||||||
items: dict[str, WishlistItem] = {}
|
def read_wishlist_items(wishlist_dir: str) -> dict[uuid.UUID, WishlistItem]:
|
||||||
path: str = f"{wishlist_dir}/wishlist.csv"
|
reservations = read_all_reservations(wishlist_dir)
|
||||||
with open(path, "r", encoding = "UTF-8") as fp:
|
|
||||||
csvreader = csv.DictReader(fp)
|
items: dict[uuid.UUID, WishlistItem] = {}
|
||||||
|
path: str = f"{wishlist_dir}/{WISHLIST_NAME}"
|
||||||
for row in csvreader:
|
with open(path, "r", encoding="UTF-8") as fp:
|
||||||
item: WishlistItem = WishlistItem()
|
csvreader = csv.DictReader(fp)
|
||||||
item.id = uuid.UUID(row.get("id"))
|
|
||||||
item.name = row.get("name")
|
for row in csvreader:
|
||||||
item.description = row.get("description")
|
item: WishlistItem = WishlistItem()
|
||||||
item.shop = row.get("shop")
|
item.id = uuid.UUID(row.get("id"))
|
||||||
item.image = row.get("image")
|
item.name = row.get("name")
|
||||||
item.price = float(row.get("price"))
|
item.description = row.get("description")
|
||||||
|
item.shop = row.get("shop")
|
||||||
items[str(item.id)] = item
|
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
|
return items
|
||||||
|
|
||||||
|
|
||||||
def read_wishlist(wishlist_dir: str) -> Wishlist:
|
def read_wishlist(wishlist_dir: str) -> Wishlist:
|
||||||
wishlist = Wishlist()
|
wishlist = Wishlist()
|
||||||
wishlist.directory = wishlist_dir
|
wishlist.directory = wishlist_dir
|
||||||
|
|
||||||
wishlist.items = read_wishlist_items(wishlist.directory)
|
wishlist.items = read_wishlist_items(wishlist.directory)
|
||||||
wishlist.config = read_wishlist_config(wishlist.directory)
|
wishlist.config = read_wishlist_config(wishlist.directory)
|
||||||
|
|
||||||
return wishlist
|
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]
|
parent_dir, wishlist_dirs, _ = list(os.walk(directory))[0]
|
||||||
|
|
||||||
wishlists: dict[str, Wishlist] = {}
|
wishlists: dict[uuid.UUID, Wishlist] = {}
|
||||||
for dir in wishlist_dirs:
|
for dir in wishlist_dirs:
|
||||||
wishlist: Wishlist = read_wishlist(f"{parent_dir}/{dir}")
|
wishlist: Wishlist = read_wishlist(f"{parent_dir}/{dir}")
|
||||||
wishlists[str(wishlist.config.id)] = wishlist
|
wishlists[wishlist.config.id] = wishlist
|
||||||
|
|
||||||
return wishlists
|
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)
|
||||||
|
|
Loading…
Reference in a new issue