From e90970adca67508cadef9cbc6c9f30a6f9babc5b Mon Sep 17 00:00:00 2001
From: nd2-trung <Nguyen5.Trung@live.uwe.ac.uk>
Date: Wed, 12 Mar 2025 12:57:07 +0700
Subject: [PATCH] add CRUD category, product

---
 app/backend/main.py             |   5 +-
 app/backend/routes/category.py  |  56 ++++++++++++
 app/backend/routes/product.py   | 153 ++++++++++++++++++++++++++++++++
 app/backend/routes/shop.py      |   7 +-
 app/backend/schemas/category.py |  18 ++++
 app/backend/schemas/product.py  |  50 +++++++++++
 requirements.txt                |   4 +-
 7 files changed, 289 insertions(+), 4 deletions(-)
 create mode 100644 app/backend/routes/category.py
 create mode 100644 app/backend/routes/product.py
 create mode 100644 app/backend/schemas/category.py
 create mode 100644 app/backend/schemas/product.py

diff --git a/app/backend/main.py b/app/backend/main.py
index 34d7e88..f903ab4 100644
--- a/app/backend/main.py
+++ b/app/backend/main.py
@@ -4,7 +4,7 @@ import os
 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 from fastapi import FastAPI
-from backend.routes import auth, shop
+from backend.routes import auth, shop, product, category
 from backend.database import init_db
 from core.config import settings
 
@@ -16,6 +16,9 @@ init_db()
 # Include API routes
 app.include_router(auth.router, prefix="/auth", tags=["auth"])
 app.include_router(shop.router, prefix="/shops", tags=["shops"])
+app.include_router(product.router, prefix="/product", tags=["product"])
+app.include_router(category.router, prefix="/category", tags=["category"])
+
 
 
 @app.get("/")
diff --git a/app/backend/routes/category.py b/app/backend/routes/category.py
new file mode 100644
index 0000000..7fbf3f3
--- /dev/null
+++ b/app/backend/routes/category.py
@@ -0,0 +1,56 @@
+from fastapi import APIRouter, Depends, HTTPException, Form
+from sqlmodel import Session
+from backend.models.models import Category
+from backend.schemas.category import  CategoryRead
+from backend.database import get_session
+
+router = APIRouter()
+
+
+@router.post("/", response_model=CategoryRead)
+def create_category(
+    name: str = Form(...),
+    session: Session = Depends(get_session),
+):
+    category = Category(name=name)
+    session.add(category)
+    session.commit()
+    session.refresh(category)
+    return category
+
+
+@router.get("/{category_id}", response_model=CategoryRead)
+def read_category(category_id: int, session: Session = Depends(get_session)):
+    category = session.get(Category, category_id)
+    if not category:
+        raise HTTPException(status_code=404, detail="Category not found")
+    return category
+
+
+@router.put("/{category_id}", response_model=CategoryRead)
+def update_category(
+    category_id: int,
+    name: str = Form(None),
+    session: Session = Depends(get_session),
+):
+    category = session.get(Category, category_id)
+    if not category:
+        raise HTTPException(status_code=404, detail="Category not found")
+
+    if name:
+        category.name = name
+    
+    session.add(category)
+    session.commit()
+    session.refresh(category)
+    return category
+
+
+@router.delete("/{category_id}")
+def delete_category(category_id: int, session: Session = Depends(get_session)):
+    category = session.get(Category, category_id)
+    if not category:
+        raise HTTPException(status_code=404, detail="Category not found")
+    session.delete(category)
+    session.commit()
+    return {"message": "Category deleted successfully"}
\ No newline at end of file
diff --git a/app/backend/routes/product.py b/app/backend/routes/product.py
new file mode 100644
index 0000000..4ccdeb6
--- /dev/null
+++ b/app/backend/routes/product.py
@@ -0,0 +1,153 @@
+from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
+from sqlmodel import Session
+from datetime import datetime
+from backend.models.models import Product, ProductImage, User
+from backend.schemas.product import ProductRead, ProductImageRead
+from backend.database import get_session
+from backend.routes.auth import get_current_user
+import shutil
+import os
+
+router = APIRouter()
+
+static_dir = os.path.join("app", "static")
+os.makedirs(static_dir, exist_ok=True)
+
+
+@router.post("/", response_model=ProductRead)
+def create_product(
+    name: str = Form(...),
+    description: str = Form(None),
+    price: float = Form(...),
+    stock: int = Form(...),
+    shop_id: int = Form(...),
+    category_id: int = Form(None),
+    images: UploadFile = File(None),  # Ensuring image is correctly set
+    session: Session = Depends(get_session),
+):
+    product = Product(
+        name=name,
+        description=description,
+        price=price,
+        stock=stock,
+        shop_id=shop_id,
+        category_id=category_id,
+        created_at=datetime.utcnow(),
+    )
+    session.add(product)
+    session.commit()
+    session.refresh(product)
+
+    # Handling image upload
+    if images and images.filename:
+        file_ext = os.path.splitext(images.filename)[1]  # Get file extension
+        file_name = f"product_{product.id}{file_ext}"
+        file_location = os.path.join(static_dir, file_name)
+
+        with open(file_location, "wb") as buffer:
+            shutil.copyfileobj(images.file, buffer)
+
+        # Save image record in ProductImage
+        product_image = ProductImage(product_id=product.id, image_url=file_location)
+        session.add(product_image)
+        session.commit()
+
+    return product
+
+
+@router.get("/{product_id}", response_model=ProductRead)
+def read_product(product_id: int, session: Session = Depends(get_session)):
+    product = session.get(Product, product_id)
+    if not product:
+        raise HTTPException(status_code=404, detail="Product not found")
+    return product
+
+
+@router.put("/{product_id}", response_model=ProductRead)
+def update_product(
+    product_id: int,
+    name: str = Form(None),
+    description: str = Form(None),
+    price: float = Form(None),
+    stock: int = Form(None),
+    category_id: int = Form(None),
+    file: UploadFile = File(None),
+    session: Session = Depends(get_session),
+):
+    product = session.get(Product, product_id)
+    if not product:
+        raise HTTPException(status_code=404, detail="Product not found")
+    
+    if name:
+        product.name = name
+    if description:
+        product.description = description
+    if price is not None:
+        product.price = price
+    if stock is not None:
+        product.stock = stock
+    if category_id is not None:
+        product.category_id = category_id
+
+    session.add(product)
+    session.commit()
+    session.refresh(product)
+
+    if file and file.filename:
+        file_location = os.path.join(static_dir, f"product_{product.id}_{file.filename}")
+        with open(file_location, "wb") as buffer:
+            shutil.copyfileobj(file.file, buffer)
+        
+        image = ProductImage(product_id=product.id, image_url=file_location)
+        session.add(image)
+        session.commit()
+
+    return product
+
+
+@router.delete("/{product_id}")
+def delete_product(product_id: int, session: Session = Depends(get_session)):
+    product = session.get(Product, product_id)
+    if not product:
+        raise HTTPException(status_code=404, detail="Product not found")
+    session.delete(product)
+    session.commit()
+    return {"message": "Product deleted successfully"}
+
+
+@router.post("/ProductImage/", response_model=ProductImageRead)
+def upload_product_image(
+    product_id: int = Form(...),
+    file: UploadFile = File(...),
+    session: Session = Depends(get_session),
+):
+    product = session.get(Product, product_id)
+    if not product:
+        raise HTTPException(status_code=404, detail="Product not found")
+
+    file_location = os.path.join(static_dir, f"product_{product.id}_{file.filename}")
+    with open(file_location, "wb") as buffer:
+        shutil.copyfileobj(file.file, buffer)
+
+    image = ProductImage(product_id=product.id, image_url=file_location)
+    session.add(image)
+    session.commit()
+    return image
+
+
+@router.get("/ProductImage/{product_id}", response_model=list[ProductImageRead])
+def get_product_images(product_id: int, session: Session = Depends(get_session)):
+    images = session.query(ProductImage).filter(ProductImage.product_id == product_id).all()
+    if not images:
+        raise HTTPException(status_code=404, detail="No images found for this product")
+    return images
+
+
+@router.delete("/ProductImage/{image_id}")
+def delete_product_image(image_id: int, session: Session = Depends(get_session)):
+    image = session.get(ProductImage, image_id)
+    if not image:
+        raise HTTPException(status_code=404, detail="Image not found")
+    session.delete(image)
+    session.commit()
+    return {"message": "Product image deleted successfully"}
diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py
index 7775a1c..ffea6d9 100644
--- a/app/backend/routes/shop.py
+++ b/app/backend/routes/shop.py
@@ -19,9 +19,12 @@ def create_shop(
     description: str = Form(None),
     file: UploadFile = File(None),
     session: Session = Depends(get_session),
-    current_user: User = Depends(get_current_user),
+    # current_user: User = Depends(get_current_user),
+    owner_id = int,
+
 ):
-    shop = ShopCreate(name=name, description=description, owner_id=current_user.id)
+    # shop = ShopCreate(name=name, description=description, owner_id=current_user.id)
+    shop = ShopCreate(name=name, description=description, owner_id=owner_id)
     db_shop = Shop.from_orm(shop)
 
     if file and file.filename:
diff --git a/app/backend/schemas/category.py b/app/backend/schemas/category.py
new file mode 100644
index 0000000..976870f
--- /dev/null
+++ b/app/backend/schemas/category.py
@@ -0,0 +1,18 @@
+from sqlmodel import SQLModel
+from typing import Optional
+
+
+class CategoryBase(SQLModel):
+    name: str
+
+
+class CategoryCreate(CategoryBase):
+    pass
+
+
+class CategoryRead(CategoryBase):
+    id: int
+
+
+class CategoryUpdate(SQLModel):
+    name: Optional[str] = None
\ No newline at end of file
diff --git a/app/backend/schemas/product.py b/app/backend/schemas/product.py
new file mode 100644
index 0000000..885907f
--- /dev/null
+++ b/app/backend/schemas/product.py
@@ -0,0 +1,50 @@
+from pydantic import BaseModel
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductBase(BaseModel):
+    shop_id: int
+    category_id: Optional[int] = None
+    name: str
+    description: Optional[str] = None
+    price: float
+    stock: int
+
+
+class ProductCreate(ProductBase):
+    pass
+
+
+class ProductRead(ProductBase):
+    id: int
+    created_at: datetime
+    images: List["ProductImageRead"] = []
+    
+    class Config:
+        orm_mode = True
+
+
+class ProductUpdate(BaseModel):
+    shop_id: Optional[int] = None
+    category_id: Optional[int] = None
+    name: Optional[str] = None
+    description: Optional[str] = None
+    price: Optional[float] = None
+    stock: Optional[int] = None
+
+
+class ProductImageBase(BaseModel):
+    product_id: int
+    image_url: str
+
+
+class ProductImageCreate(ProductImageBase):
+    pass
+
+
+class ProductImageRead(ProductImageBase):
+    id: int
+    
+    class Config:
+        orm_mode = True
diff --git a/requirements.txt b/requirements.txt
index 7dfdd9c..c8b7950 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ cffi==1.17.1
 charset-normalizer==3.4.1
 click==8.1.8
 colorama==0.4.6
+cryptography==44.0.2
 dnspython==2.7.0
 email_validator==2.2.0
 fastapi==0.115.11
@@ -31,6 +32,7 @@ pydantic-extra-types==2.10.2
 pydantic-settings==2.8.1
 pydantic_core==2.27.2
 Pygments==2.19.1
+PyJWT==2.10.1
 PyMySQL==1.1.1
 python-dotenv==1.0.1
 python-multipart==0.0.20
@@ -50,4 +52,4 @@ ujson==5.10.0
 urllib3==2.3.0
 uvicorn==0.34.0
 watchfiles==1.0.4
-websockets==15.0
+websockets==15.0
\ No newline at end of file
-- 
GitLab