Skip to content

Commit

Permalink
Hook database queries to the products page
Browse files Browse the repository at this point in the history
  • Loading branch information
exflikt committed Oct 10, 2024
1 parent 1d56634 commit aff3f27
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 87 deletions.
70 changes: 61 additions & 9 deletions app/routers/products.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,77 @@
from fastapi import APIRouter, Request
from typing import Annotated

from fastapi import APIRouter, Form, HTTPException, Request, Response, status
from fastapi.responses import HTMLResponse

from .. import templates
from ..store import ProductTable
from ..store import Product, ProductTable, delete_product

router = APIRouter()


@router.get("/products", response_class=HTMLResponse)
async def get_products(request: Request):
products = await ProductTable.select_all()
return HTMLResponse(templates.products(request, products))
return HTMLResponse(templates.products.page(request, products))


@router.post("/products", response_class=Response)
async def new_product(
product_id: Annotated[int, Form()],
name: Annotated[str, Form(max_length=40)],
filename: Annotated[str, Form(max_length=100)],
price: Annotated[int, Form()],
no_stock: Annotated[int | None, Form()] = None,
):
new_product = Product(
product_id=product_id,
name=name,
filename=filename,
price=price,
no_stock=no_stock,
)
await ProductTable.insert(new_product)
return Response(headers={"hx-refresh": "true"})


@router.post("/products/{product_id}", response_class=Response)
async def update_product(
product_id: int,
new_product_id: Annotated[int, Form()],
name: Annotated[str, Form(max_length=40)],
filename: Annotated[str, Form(max_length=100)],
price: Annotated[int, Form()],
no_stock: Annotated[int | None, Form()] = None,
):
new_product = Product(
product_id=new_product_id,
name=name,
filename=filename,
price=price,
no_stock=no_stock,
)

# TODO: Test whether the returning SQL clause in update function is working or not
await ProductTable.update(product_id, new_product)
return Response(headers={"hx-refresh": "true"})

# @router.post("/products/{product_id}", response_class=HTMLResponse)
# async def update_product(request: Request, product_id: int):
# product = await ProductTable.by_product_id(product_id)
# return HTMLResponse(templates.components.product(request, product))

@router.delete("/products/{product_id}", response_class=Response)
async def delete(product_id: int):
await delete_product(product_id)
return Response(headers={"hx-refresh": "true"})


@router.get("/products/{product_id}/editor", response_class=HTMLResponse)
async def get_product_editor(request: Request, product_id: int):
product = await ProductTable.by_product_id(product_id)
return HTMLResponse(templates.components.product_editor(request, product))
maybe_product = await ProductTable.by_product_id(product_id)

if (product := maybe_product) is None:
detail = f"Product {product_id} not found"
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
return HTMLResponse(templates.products.editor(request, product))


@router.get("/product-editor", response_class=HTMLResponse)
async def get_empty_product_editor(request: Request):
return HTMLResponse(templates.products.empty_editor(request))
10 changes: 10 additions & 0 deletions app/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
PlacementTable = placement.Table(database)


async def delete_product(product_id: int):
async with database.transaction():
query = sqlmodel.delete(Product).where(col(Product.product_id) == product_id)
await database.execute(query)

clause = col(PlacedItem.product_id) == product_id
query = sqlmodel.delete(PlacedItem).where(clause)
await database.execute(query)


def _to_time(unix_epoch: int) -> str:
return datetime.fromtimestamp(unix_epoch).strftime("%H:%M:%S")

Expand Down
32 changes: 29 additions & 3 deletions app/store/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pydantic
import sqlmodel
from databases import Database
from sqlmodel import col


class Product(sqlmodel.SQLModel, table=True):
Expand All @@ -12,10 +13,8 @@ class Product(sqlmodel.SQLModel, table=True):

id: int | None = sqlmodel.Field(default=None, primary_key=True)
product_id: int
# Column(..., String(length=40))
name: Annotated[str, sqlmodel.Field(max_length=40)]
# Column(..., String(length=100))
filename: Annotated[str, sqlmodel.Field(max_length=40)]
filename: Annotated[str, sqlmodel.Field(max_length=100)]
price: int
no_stock: int | None # Column(..., nullable=True)

Expand Down Expand Up @@ -218,3 +217,30 @@ async def by_product_id(self, product_id: int) -> Product | None:
query = sqlmodel.select(Product).where(Product.product_id == product_id)
row = await self._db.fetch_one(query)
return Product.model_validate(row) if row else None

async def insert(self, product: Product) -> None:
query = sqlmodel.insert(Product)
await self._db.execute(query, product.model_dump())

async def update(self, product_id: int, new_product: Product) -> int | None:
dump = new_product.model_dump()
dump.pop("id")

query = (
sqlmodel.update(Product)
.where(col(Product.product_id) == product_id)
.values(**dump)
.returning(col(Product.product_id))
)
if product_id != new_product.product_id:
query = query.where(
sqlmodel.not_(
sqlmodel.exists(
sqlmodel.select(col(Product.product_id)).where(
col(Product.product_id) == new_product.product_id
)
)
)
)

return await self._db.execute(query)
18 changes: 12 additions & 6 deletions app/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,18 @@ def layout(
def index(): ...


@macro_template("products.html")
def products(products: list[Product]): ...
class products: # namespace
@macro_template("products.html")
@staticmethod
def page(products: list[Product]): ...

@macro_template("products.html", "editor")
@staticmethod
def editor(product: Product): ...

@macro_template("products.html", "empty_editor")
@staticmethod
def empty_editor(): ...


class order: # namespace
Expand Down Expand Up @@ -148,10 +158,6 @@ def wait_estimates(): ...


class components: # namespace
@macro_template("components/product-editor.html")
@staticmethod
def product_editor(product: Product | None): ...

@macro_template("components/order-confirm.html")
@staticmethod
def order_confirm(session: OrderSession, error_status: Optional[str]): ...
Expand Down
64 changes: 0 additions & 64 deletions app/templates/components/product-editor.html

This file was deleted.

101 changes: 99 additions & 2 deletions app/templates/products.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@
<div class="text-center">{{ product.price_str() }}</div>
</figure>
{% endfor %}
<figure
id="product-placeholder"
hx-get="/product-editor"
hx-target="#product-editor"
class="flex flex-col border-4 border-blue-400 rounded-md transition-colors ease-in-out active:bg-blue-300"
>
<img class="mx-auto w-full h-auto aspect-square" src="{{ url_for('static', path='no-image.png') }}" alt="新しい商品を追加" />
<figcaption class="text-center truncate">新しい商品を追加する</figcaption>
<div class="text-center"> </div>
</figure>
</main>
<aside class="w-2/6 flex flex-col p-4">
<ul class="text-xl">
<li><a href="/" class="cursor-pointer">ホーム</a></li>
<ul class="flex flex-row py-2 justify-around items-center text-xl">
<li class="flex-grow"><a href="/" class="px-2 py-1 rounded bg-gray-300 hidden lg:inline-block">ホーム</a></li>
</ul>
<div id="product-editor" class="grow flex flex-col justify-center">
<div class="text-center">
Expand All @@ -32,3 +42,90 @@
</div>
{% endcall %}
{% endmacro %}

{% macro editor(product) %}
<div class="h-full flex flex-col">
<div id="product-preview" class="flex-grow">
<figure class="w-2/3 mx-auto flex flex-col border-4 border-blue-400 rounded-md transition-colors ease-in-out active:bg-blue-300">
<img class="mx-auto w-full h-auto aspect-square" src="{{ url_for('static', path=product.filename) }}" alt="{{ product.name }}"/>
<figcaption class="text-center truncate">{{ product.name }}</figcaption>
<div class="text-center">{{ product.price_str() }}</div>
</figure>
</div>
<form
hx-post="/products/{{ product.product_id }}"
class="flex flex-col"
>
<fieldset class="grid grid-cols-3 auto-rows-min justify-items-end gap-2 py-4 text-lg">
<label for="product-id" class="w-full text-right">商品番号:</label>
<input type="number" id="product-id" name="new_product_id" value="{{ product.product_id }}" class="col-span-2 w-full"/>
<label for="product-name" class="w-full text-right">商品名:</label>
<input type="text" id="product-name" name="name" value="{{ product.name }}" class="col-span-2 w-full"/>
<label for="product-filename" class="w-full text-right">ファイル名:</label>
<input type="text" id="product-filename" name="filename" value="{{ product.filename }}" class="col-span-2 w-full"/>
<label for="product-price" class="w-full text-right">金額:</label>
<input type="number" id="product-price" name="price" value="{{ product.price }}" class="col-span-2 w-full"/>
<label for="product-no-stock" class="w-full text-right truncate">在庫数(未実装):</label>
<input type="text" id="product-no-stock" name="no_stock" value="{{ product.no_stock or '' }}" class="col-span-2 w-full"/>
</fieldset>
<div class="flex flex-row justify-between">
<button
hx-delete="/products/{{ product.product_id }}"
hx-swap="none"
hx-confirm="本当に「{{ product.name }}({{ product.product_id }})」を削除しますか?"
type="button"
class="px-2 py-1 text-white bg-red-500 rounded-lg"
>削除</button>
<button
hx-get="/products/{{ product.product_id }}/editor"
hx-target="#product-editor"
type="reset"
class="px-2 py-1 border border-black rounded-lg"
>リセット</button>
<button
type="submit"
class="px-2 py-1 text-white bg-blue-500 rounded-lg"
>更新</button>
</div>
</form>
</div>
{% endmacro %}

{% macro empty_editor() %}
<div class="h-full flex flex-col">
<div id="product-preview" class="flex-grow">
<figure class="w-2/3 mx-auto flex flex-col border-4 border-blue-400 rounded-md transition-colors ease-in-out active:bg-blue-300">
<img class="mx-auto w-full h-auto aspect-square" src="{{ url_for('static', path='no-image.png') }}" alt="仮画像"/>
</figure>
</div>
<form
hx-post="/products"
class="flex flex-col"
>
<fieldset class="grid grid-cols-3 auto-rows-min justify-items-end gap-2 py-4 text-lg">
<label for="product-id" class="w-full text-right">商品番号:</label>
<input type="number" id="product-id" name="product_id" class="col-span-2 w-full"/>
<label for="product-name" class="w-full text-right">商品名:</label>
<input type="text" id="product-name" name="name" class="col-span-2 w-full"/>
<label for="product-filename" class="w-full text-right">ファイル名:</label>
<input type="text" id="product-filename" name="filename" value="no-image.png" class="col-span-2 w-full"/>
<label for="product-price" class="w-full text-right">金額:</label>
<input type="number" id="product-price" name="price" value="0" class="col-span-2 w-full"/>
<label for="product-no-stock" class="w-full text-right truncate">在庫数(未実装):</label>
<input type="text" id="product-no-stock" name="no_stock" class="col-span-2 w-full"/>
</fieldset>
<div class="flex flex-row justify-between">
<button
hx-get="/product-editor"
hx-target="#product-editor"
type="reset"
class="px-2 py-1 border border-black rounded-lg"
>リセット</button>
<button
type="submit"
class="px-2 py-1 text-white bg-blue-500 rounded-lg"
>追加</button>
</div>
</form>
</div>
{% endmacro %}
Binary file added static/no-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit aff3f27

Please sign in to comment.