Skip to content

Commit

Permalink
Merge pull request #7 from databases-CSCI3354/add-to-cart
Browse files Browse the repository at this point in the history
Add to cart
  • Loading branch information
jinyang628 authored Feb 7, 2025
2 parents e77d001 + 3b8b0eb commit c6cca2c
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 29 deletions.
3 changes: 3 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import secrets

from flask import Flask

Expand All @@ -9,6 +10,8 @@
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
secret_key = secrets.token_hex(32)
app.secret_key = secret_key
app.config["DATABASE"] = os.path.join(app.root_path, "northwind.db")
app.teardown_appcontext(close_db)
init_app(app=app)
Expand Down
11 changes: 11 additions & 0 deletions app/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ class Product(BaseModel):
UnitsOnOrder: int
ReorderLevel: int
Discontinued: str


class CartItem(BaseModel):
ProductID: int
Quantity: int
ProductName: str
TotalPrice: float


class Cart(BaseModel):
items: dict[int, CartItem]
2 changes: 1 addition & 1 deletion app/routes/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import Blueprint, render_template

from app.models.product import Product
from app.services.products import ProductService
from app.services.product import ProductService

main_bp = Blueprint("main", __name__)

Expand Down
47 changes: 43 additions & 4 deletions app/routes/product.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,61 @@
from typing import Optional

from flask import Blueprint, render_template
from flask import Blueprint, jsonify, render_template, request

from app.models.category import Category
from app.models.product import Product
from app.models.product import CartItem, Product
from app.models.supplier import Supplier
from app.services.cart import save_item_to_cart
from app.services.category import CategoryService
from app.services.products import ProductService
from app.services.product import ProductService
from app.services.supplier import SupplierService
from app.utils.logger import setup_logger

product_bp = Blueprint("product", __name__)


log = setup_logger(__name__)


@product_bp.route("/<int:product_id>")
def index(product_id: int):
product: Product = ProductService().get_product_by_id(product_id)
product: Optional[Product] = ProductService().get_product_by_id(product_id)
if not product:
raise ValueError(f"Error rendering product page: product not found with id {product_id}")
category: Optional[Category] = CategoryService().get_category_by_id(product.CategoryID)
supplier: Optional[Supplier] = SupplierService().get_supplier_by_id(product.SupplierID)
return render_template(
"product/index.html", product=product, category=category, supplier=supplier
)


@product_bp.route("/<int:product_id>", methods=["POST"])
def add_to_cart(product_id):
product: Optional[Product] = ProductService().get_product_by_id(product_id)
if not product:
raise ValueError(f"Error adding product to cart: product not found with id {product_id}")

quantity = int(request.form.get("quantity", 1))

if quantity > product.UnitsInStock:
raise ValueError(
f"Error adding product to cart: requested quantity exceeds available stock"
)

cart_item = CartItem(
ProductID=product_id,
Quantity=quantity,
ProductName=product.ProductName,
TotalPrice=product.UnitPrice * quantity,
)

save_item_to_cart(cart_item=cart_item)
log.info(f"Added the following item to cart: {cart_item}")

# category: Optional[Category] = CategoryService().get_category_by_id(product.CategoryID)
# supplier: Optional[Supplier] = SupplierService().get_supplier_by_id(product.SupplierID)
# return render_template(
# "product/index.html", product=product, category=category, supplier=supplier
# )

return jsonify({"message": f"Added {product.ProductName} to cart"})
15 changes: 15 additions & 0 deletions app/services/cart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from flask import session

from app.models.product import Cart, CartItem


def get_cart() -> Cart:
if "cart" not in session:
session["cart"] = Cart(items={})
return Cart.model_validate(session["cart"])


def save_item_to_cart(cart_item: CartItem) -> None:
cart = get_cart()
cart.items[cart_item.ProductID] = cart_item
session["cart"] = cart.model_dump()
39 changes: 39 additions & 0 deletions app/services/product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Optional

from app.models.product import Product
from app.utils.database import get_db
from app.utils.logger import setup_logger

log = setup_logger(__name__)


class ProductService:

def __init__(self):
self.cursor = get_db().cursor()
self.columns = list(Product.model_fields.keys())

def get_all_products(self) -> list[Product]:
self.cursor.execute("SELECT * FROM Products")
products: list[Product] = [
Product.model_validate(dict(zip(self.columns, row))) for row in self.cursor.fetchall()
]
return products

def get_product_by_id(self, product_id: Optional[int]) -> Optional[Product]:
if not product_id:
return None
self.cursor.execute(f"SELECT * FROM Products WHERE ProductID = {product_id}")
rows: list = self.cursor.fetchall()
match len(rows):
case 0:
log.info(f"No product found with id {product_id}")
return None
case 1:
product: Product = Product.model_validate(dict(zip(self.columns, rows[0])))
return product
case _:
log.error(
f"Invalid number of rows returned for product with id {product_id}: {len(rows)}"
)
raise ValueError("Multiple rows returned")
21 changes: 0 additions & 21 deletions app/services/products.py

This file was deleted.

6 changes: 3 additions & 3 deletions app/templates/base.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!doctype html>
<title>{% block title %}{% endblock %} - Online Commerce Site</title>
<title>{% block title %}{% endblock %} - Walmart</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
<h1>Online Commerce Site</h1>
<h1>Walmart</h1>
<ul>

</ul>
Expand All @@ -15,4 +15,4 @@ <h1>Online Commerce Site</h1>
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</section>
</section>
48 changes: 48 additions & 0 deletions app/templates/product/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,56 @@ <h2 class="card-title">{{ product.ProductName }}</h2>
<p>{{ product.QuantityPerUnit }}</p>
</div>
</div>

<form action="{{ url_for('product.add_to_cart', product_id=product.ProductID) }}" method="POST">
<div class="d-flex align-items-center mt-3">

<button type="button" class="btn btn-outline-secondary" id="decrease-qty">-</button>
<input type="number" name="quantity" id="quantity" class="form-control mx-2 text-center" min="1"
max="{{ product.UnitsInStock }}" value="1" style="width: 80px;" readonly>
<button type="button" class="btn btn-outline-secondary" id="increase-qty">+</button>

<button type="submit" class="btn btn-primary ml-3" id="add-to-cart">
Add to Cart
</button>
</div>

</form>

<a href="{{ url_for('main.index') }}" class="btn btn-secondary mt-3">Back to Products</a>
</div>
</div>
</div>

<style>
/* Hide the spinners (up/down arrows) in the number input */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>

<script>
document.addEventListener('DOMContentLoaded', function () {
const quantityInput = document.getElementById('quantity');
const decreaseButton = document.getElementById('decrease-qty');
const increaseButton = document.getElementById('increase-qty');

decreaseButton.addEventListener('click', function () {
let currentValue = parseInt(quantityInput.value);
if (currentValue > 1) {
quantityInput.value = currentValue - 1;
}
});

increaseButton.addEventListener('click', function () {
let currentValue = parseInt(quantityInput.value);
if (currentValue < parseInt(quantityInput.max)) {
quantityInput.value = currentValue + 1;
}
});
});
</script>

{% endblock %}
13 changes: 13 additions & 0 deletions app/utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import logging


def setup_logger(name: str) -> logging.Logger:
"""Setup a logger with basic formatting."""
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter("%(levelname)-8s [%(name)s] %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.propagate = False
return logger

0 comments on commit c6cca2c

Please sign in to comment.