remove intermediate grocery table and add related_products feature
This commit is contained in:
274
backend/main.py
274
backend/main.py
@@ -26,21 +26,18 @@ app.add_middleware(
|
||||
|
||||
def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> schemas.ShoppingEventResponse:
|
||||
"""Build a shopping event response with products from the association table"""
|
||||
# Get products with their event-specific data including grocery and brand information
|
||||
# Get products with their event-specific data including category and brand information
|
||||
product_data = db.execute(
|
||||
text("""
|
||||
SELECT p.id, p.name, p.organic, p.weight, p.weight_unit,
|
||||
sep.amount, sep.price,
|
||||
g.id as grocery_id, g.name as grocery_name,
|
||||
g.created_at as grocery_created_at, g.updated_at as grocery_updated_at,
|
||||
gc.id as category_id, gc.name as category_name,
|
||||
gc.created_at as category_created_at, gc.updated_at as category_updated_at,
|
||||
b.id as brand_id, b.name as brand_name,
|
||||
b.created_at as brand_created_at, b.updated_at as brand_updated_at
|
||||
FROM products p
|
||||
JOIN shopping_event_products sep ON p.id = sep.product_id
|
||||
JOIN groceries g ON p.grocery_id = g.id
|
||||
JOIN grocery_categories gc ON g.category_id = gc.id
|
||||
JOIN grocery_categories gc ON p.category_id = gc.id
|
||||
LEFT JOIN brands b ON p.brand_id = b.id
|
||||
WHERE sep.shopping_event_id = :event_id
|
||||
"""),
|
||||
@@ -57,15 +54,6 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s
|
||||
updated_at=row.category_updated_at
|
||||
)
|
||||
|
||||
grocery = schemas.Grocery(
|
||||
id=row.grocery_id,
|
||||
name=row.grocery_name,
|
||||
category_id=row.category_id,
|
||||
created_at=row.grocery_created_at,
|
||||
updated_at=row.grocery_updated_at,
|
||||
category=category
|
||||
)
|
||||
|
||||
brand = None
|
||||
if row.brand_id is not None:
|
||||
brand = schemas.Brand(
|
||||
@@ -79,7 +67,7 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s
|
||||
schemas.ProductWithEventData(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
grocery=grocery,
|
||||
category=category,
|
||||
brand=brand,
|
||||
organic=row.organic,
|
||||
weight=row.weight,
|
||||
@@ -108,10 +96,10 @@ def read_root():
|
||||
# Product endpoints
|
||||
@app.post("/products/", response_model=schemas.Product)
|
||||
def create_product(product: schemas.ProductCreate, db: Session = Depends(get_db)):
|
||||
# Validate grocery exists
|
||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == product.grocery_id).first()
|
||||
if grocery is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
||||
# Validate category exists
|
||||
category = db.query(models.GroceryCategory).filter(models.GroceryCategory.id == product.category_id).first()
|
||||
if category is None:
|
||||
raise HTTPException(status_code=404, detail="Category not found")
|
||||
|
||||
# Validate brand exists if brand_id is provided
|
||||
if product.brand_id is not None:
|
||||
@@ -145,11 +133,11 @@ def update_product(product_id: int, product_update: schemas.ProductUpdate, db: S
|
||||
|
||||
update_data = product_update.dict(exclude_unset=True)
|
||||
|
||||
# Validate grocery exists if grocery_id is being updated
|
||||
if 'grocery_id' in update_data:
|
||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == update_data['grocery_id']).first()
|
||||
if grocery is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
||||
# Validate category exists if category_id is being updated
|
||||
if 'category_id' in update_data:
|
||||
category = db.query(models.GroceryCategory).filter(models.GroceryCategory.id == update_data['category_id']).first()
|
||||
if category is None:
|
||||
raise HTTPException(status_code=404, detail="Category not found")
|
||||
|
||||
# Validate brand exists if brand_id is being updated
|
||||
if 'brand_id' in update_data and update_data['brand_id'] is not None:
|
||||
@@ -382,83 +370,18 @@ def delete_grocery_category(category_id: int, db: Session = Depends(get_db)):
|
||||
if category is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery category not found")
|
||||
|
||||
# Check if any groceries reference this category
|
||||
groceries_with_category = db.query(models.Grocery).filter(models.Grocery.category_id == category_id).first()
|
||||
if groceries_with_category:
|
||||
# Check if any products reference this category
|
||||
products_with_category = db.query(models.Product).filter(models.Product.category_id == category_id).first()
|
||||
if products_with_category:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete category: groceries are still associated with this category"
|
||||
detail="Cannot delete category: products are still associated with this category"
|
||||
)
|
||||
|
||||
db.delete(category)
|
||||
db.commit()
|
||||
return {"message": "Grocery category deleted successfully"}
|
||||
|
||||
# Grocery endpoints
|
||||
@app.post("/groceries/", response_model=schemas.Grocery)
|
||||
def create_grocery(grocery: schemas.GroceryCreate, db: Session = Depends(get_db)):
|
||||
# Validate category exists
|
||||
category = db.query(models.GroceryCategory).filter(models.GroceryCategory.id == grocery.category_id).first()
|
||||
if category is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery category not found")
|
||||
|
||||
db_grocery = models.Grocery(**grocery.dict())
|
||||
db.add(db_grocery)
|
||||
db.commit()
|
||||
db.refresh(db_grocery)
|
||||
return db_grocery
|
||||
|
||||
@app.get("/groceries/", response_model=List[schemas.Grocery])
|
||||
def read_groceries(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
groceries = db.query(models.Grocery).offset(skip).limit(limit).all()
|
||||
return groceries
|
||||
|
||||
@app.get("/groceries/{grocery_id}", response_model=schemas.Grocery)
|
||||
def read_grocery(grocery_id: int, db: Session = Depends(get_db)):
|
||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
|
||||
if grocery is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
||||
return grocery
|
||||
|
||||
@app.put("/groceries/{grocery_id}", response_model=schemas.Grocery)
|
||||
def update_grocery(grocery_id: int, grocery_update: schemas.GroceryUpdate, db: Session = Depends(get_db)):
|
||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
|
||||
if grocery is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
||||
|
||||
update_data = grocery_update.dict(exclude_unset=True)
|
||||
|
||||
# Validate category exists if category_id is being updated
|
||||
if 'category_id' in update_data:
|
||||
category = db.query(models.GroceryCategory).filter(models.GroceryCategory.id == update_data['category_id']).first()
|
||||
if category is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery category not found")
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(grocery, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(grocery)
|
||||
return grocery
|
||||
|
||||
@app.delete("/groceries/{grocery_id}")
|
||||
def delete_grocery(grocery_id: int, db: Session = Depends(get_db)):
|
||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
|
||||
if grocery is None:
|
||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
||||
|
||||
# Check if any products reference this grocery
|
||||
products_with_grocery = db.query(models.Product).filter(models.Product.grocery_id == grocery_id).first()
|
||||
if products_with_grocery:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete grocery: products are still associated with this grocery"
|
||||
)
|
||||
|
||||
db.delete(grocery)
|
||||
db.commit()
|
||||
return {"message": "Grocery deleted successfully"}
|
||||
|
||||
# Shopping Event endpoints
|
||||
@app.post("/shopping-events/", response_model=schemas.ShoppingEventResponse)
|
||||
def create_shopping_event(event: schemas.ShoppingEventCreate, db: Session = Depends(get_db)):
|
||||
@@ -584,6 +507,171 @@ def get_shop_stats(db: Session = Depends(get_db)):
|
||||
# This would need more complex SQL query - placeholder for now
|
||||
return []
|
||||
|
||||
# Related Products endpoints
|
||||
@app.post("/related-products/", response_model=schemas.RelatedProduct)
|
||||
def create_related_product(related_product: schemas.RelatedProductCreate, db: Session = Depends(get_db)):
|
||||
# Validate both products exist
|
||||
product = db.query(models.Product).filter(models.Product.id == related_product.product_id).first()
|
||||
if product is None:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
related = db.query(models.Product).filter(models.Product.id == related_product.related_product_id).first()
|
||||
if related is None:
|
||||
raise HTTPException(status_code=404, detail="Related product not found")
|
||||
|
||||
# Prevent self-referencing
|
||||
if related_product.product_id == related_product.related_product_id:
|
||||
raise HTTPException(status_code=400, detail="A product cannot be related to itself")
|
||||
|
||||
# Check if relationship already exists (in either direction)
|
||||
existing = db.execute(
|
||||
models.related_products.select().where(
|
||||
((models.related_products.c.product_id == related_product.product_id) &
|
||||
(models.related_products.c.related_product_id == related_product.related_product_id)) |
|
||||
((models.related_products.c.product_id == related_product.related_product_id) &
|
||||
(models.related_products.c.related_product_id == related_product.product_id))
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Products are already related")
|
||||
|
||||
# Insert the relationship
|
||||
result = db.execute(
|
||||
models.related_products.insert().values(
|
||||
product_id=related_product.product_id,
|
||||
related_product_id=related_product.related_product_id,
|
||||
relationship_type=related_product.relationship_type
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# Get the created relationship
|
||||
relationship_id = result.inserted_primary_key[0]
|
||||
created_relationship = db.execute(
|
||||
models.related_products.select().where(models.related_products.c.id == relationship_id)
|
||||
).first()
|
||||
|
||||
return schemas.RelatedProduct(
|
||||
id=created_relationship.id,
|
||||
product_id=created_relationship.product_id,
|
||||
related_product_id=created_relationship.related_product_id,
|
||||
relationship_type=created_relationship.relationship_type,
|
||||
created_at=created_relationship.created_at
|
||||
)
|
||||
|
||||
@app.get("/related-products/", response_model=List[schemas.RelatedProduct])
|
||||
def read_related_products(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
relationships = db.execute(
|
||||
models.related_products.select().offset(skip).limit(limit)
|
||||
).fetchall()
|
||||
|
||||
return [
|
||||
schemas.RelatedProduct(
|
||||
id=rel.id,
|
||||
product_id=rel.product_id,
|
||||
related_product_id=rel.related_product_id,
|
||||
relationship_type=rel.relationship_type,
|
||||
created_at=rel.created_at
|
||||
)
|
||||
for rel in relationships
|
||||
]
|
||||
|
||||
@app.get("/related-products/product/{product_id}", response_model=List[schemas.Product])
|
||||
def get_related_products_for_product(product_id: int, db: Session = Depends(get_db)):
|
||||
# Validate product exists
|
||||
product = db.query(models.Product).filter(models.Product.id == product_id).first()
|
||||
if product is None:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# Get related products (bidirectional)
|
||||
related_product_ids = db.execute(
|
||||
text("""
|
||||
SELECT CASE
|
||||
WHEN product_id = :product_id THEN related_product_id
|
||||
ELSE product_id
|
||||
END as related_id
|
||||
FROM related_products
|
||||
WHERE product_id = :product_id OR related_product_id = :product_id
|
||||
"""),
|
||||
{"product_id": product_id}
|
||||
).fetchall()
|
||||
|
||||
if not related_product_ids:
|
||||
return []
|
||||
|
||||
# Get the actual product objects
|
||||
related_ids = [row.related_id for row in related_product_ids]
|
||||
related_products = db.query(models.Product).filter(models.Product.id.in_(related_ids)).all()
|
||||
|
||||
return related_products
|
||||
|
||||
@app.get("/related-products/{relationship_id}", response_model=schemas.RelatedProduct)
|
||||
def read_related_product(relationship_id: int, db: Session = Depends(get_db)):
|
||||
relationship = db.execute(
|
||||
models.related_products.select().where(models.related_products.c.id == relationship_id)
|
||||
).first()
|
||||
|
||||
if relationship is None:
|
||||
raise HTTPException(status_code=404, detail="Related product relationship not found")
|
||||
|
||||
return schemas.RelatedProduct(
|
||||
id=relationship.id,
|
||||
product_id=relationship.product_id,
|
||||
related_product_id=relationship.related_product_id,
|
||||
relationship_type=relationship.relationship_type,
|
||||
created_at=relationship.created_at
|
||||
)
|
||||
|
||||
@app.put("/related-products/{relationship_id}", response_model=schemas.RelatedProduct)
|
||||
def update_related_product(relationship_id: int, update_data: schemas.RelatedProductUpdate, db: Session = Depends(get_db)):
|
||||
# Check if relationship exists
|
||||
existing = db.execute(
|
||||
models.related_products.select().where(models.related_products.c.id == relationship_id)
|
||||
).first()
|
||||
|
||||
if existing is None:
|
||||
raise HTTPException(status_code=404, detail="Related product relationship not found")
|
||||
|
||||
# Update the relationship
|
||||
db.execute(
|
||||
models.related_products.update().where(models.related_products.c.id == relationship_id).values(
|
||||
relationship_type=update_data.relationship_type
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# Get the updated relationship
|
||||
updated_relationship = db.execute(
|
||||
models.related_products.select().where(models.related_products.c.id == relationship_id)
|
||||
).first()
|
||||
|
||||
return schemas.RelatedProduct(
|
||||
id=updated_relationship.id,
|
||||
product_id=updated_relationship.product_id,
|
||||
related_product_id=updated_relationship.related_product_id,
|
||||
relationship_type=updated_relationship.relationship_type,
|
||||
created_at=updated_relationship.created_at
|
||||
)
|
||||
|
||||
@app.delete("/related-products/{relationship_id}")
|
||||
def delete_related_product(relationship_id: int, db: Session = Depends(get_db)):
|
||||
# Check if relationship exists
|
||||
existing = db.execute(
|
||||
models.related_products.select().where(models.related_products.c.id == relationship_id)
|
||||
).first()
|
||||
|
||||
if existing is None:
|
||||
raise HTTPException(status_code=404, detail="Related product relationship not found")
|
||||
|
||||
# Delete the relationship
|
||||
db.execute(
|
||||
models.related_products.delete().where(models.related_products.c.id == relationship_id)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Related product relationship deleted successfully"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@@ -17,6 +17,17 @@ shopping_event_products = Table(
|
||||
Column('price', Float, nullable=False) # Price of this product at the time of this shopping event
|
||||
)
|
||||
|
||||
# Association table for many-to-many self-referential relationship between related products
|
||||
related_products = Table(
|
||||
'related_products',
|
||||
Base.metadata,
|
||||
Column('id', Integer, primary_key=True, autoincrement=True), # Artificial primary key
|
||||
Column('product_id', Integer, ForeignKey('products.id'), nullable=False),
|
||||
Column('related_product_id', Integer, ForeignKey('products.id'), nullable=False),
|
||||
Column('relationship_type', String, nullable=True), # Optional: e.g., "size_variant", "brand_variant", "similar"
|
||||
Column('created_at', DateTime(timezone=True), server_default=func.now())
|
||||
)
|
||||
|
||||
class BrandInShop(Base):
|
||||
__tablename__ = "brands_in_shops"
|
||||
|
||||
@@ -51,27 +62,14 @@ class GroceryCategory(Base):
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
groceries = relationship("Grocery", back_populates="category")
|
||||
|
||||
class Grocery(Base):
|
||||
__tablename__ = "groceries"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False, index=True)
|
||||
category_id = Column(Integer, ForeignKey("grocery_categories.id"), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
category = relationship("GroceryCategory", back_populates="groceries")
|
||||
products = relationship("Product", back_populates="grocery")
|
||||
products = relationship("Product", back_populates="category")
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False, index=True)
|
||||
grocery_id = Column(Integer, ForeignKey("groceries.id"), nullable=False)
|
||||
category_id = Column(Integer, ForeignKey("grocery_categories.id"), nullable=False)
|
||||
brand_id = Column(Integer, ForeignKey("brands.id"), nullable=True)
|
||||
organic = Column(Boolean, default=False)
|
||||
weight = Column(Float, nullable=True) # in grams or kg
|
||||
@@ -80,9 +78,26 @@ class Product(Base):
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
grocery = relationship("Grocery", back_populates="products")
|
||||
category = relationship("GroceryCategory", back_populates="products")
|
||||
brand = relationship("Brand", back_populates="products")
|
||||
shopping_events = relationship("ShoppingEvent", secondary=shopping_event_products, back_populates="products")
|
||||
|
||||
# Self-referential many-to-many relationship for related products
|
||||
related_products = relationship(
|
||||
"Product",
|
||||
secondary=related_products,
|
||||
primaryjoin=id == related_products.c.product_id,
|
||||
secondaryjoin=id == related_products.c.related_product_id,
|
||||
back_populates="related_to_products"
|
||||
)
|
||||
|
||||
related_to_products = relationship(
|
||||
"Product",
|
||||
secondary=related_products,
|
||||
primaryjoin=id == related_products.c.related_product_id,
|
||||
secondaryjoin=id == related_products.c.product_id,
|
||||
back_populates="related_products"
|
||||
)
|
||||
|
||||
class Shop(Base):
|
||||
__tablename__ = "shops"
|
||||
|
||||
@@ -60,31 +60,10 @@ class GroceryCategory(GroceryCategoryBase):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Grocery schemas
|
||||
class GroceryBase(BaseModel):
|
||||
name: str
|
||||
category_id: int
|
||||
|
||||
class GroceryCreate(GroceryBase):
|
||||
pass
|
||||
|
||||
class GroceryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
category_id: Optional[int] = None
|
||||
|
||||
class Grocery(GroceryBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
category: GroceryCategory
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Base schemas
|
||||
# Product schemas
|
||||
class ProductBase(BaseModel):
|
||||
name: str
|
||||
grocery_id: int
|
||||
category_id: int
|
||||
brand_id: Optional[int] = None
|
||||
organic: bool = False
|
||||
weight: Optional[float] = None
|
||||
@@ -95,7 +74,7 @@ class ProductCreate(ProductBase):
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
grocery_id: Optional[int] = None
|
||||
category_id: Optional[int] = None
|
||||
brand_id: Optional[int] = None
|
||||
organic: Optional[bool] = None
|
||||
weight: Optional[float] = None
|
||||
@@ -105,7 +84,7 @@ class Product(ProductBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
grocery: Grocery
|
||||
category: GroceryCategory
|
||||
brand: Optional[Brand] = None
|
||||
|
||||
class Config:
|
||||
@@ -142,7 +121,7 @@ class ProductInEvent(BaseModel):
|
||||
class ProductWithEventData(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
grocery: Grocery
|
||||
category: GroceryCategory
|
||||
brand: Optional[Brand] = None
|
||||
organic: bool
|
||||
weight: Optional[float] = None
|
||||
@@ -193,4 +172,30 @@ class ShopStats(BaseModel):
|
||||
avg_per_visit: float
|
||||
|
||||
# Update forward references
|
||||
BrandInShop.model_rebuild()
|
||||
BrandInShop.model_rebuild()
|
||||
|
||||
# Related Products schemas
|
||||
class RelatedProductBase(BaseModel):
|
||||
product_id: int
|
||||
related_product_id: int
|
||||
relationship_type: Optional[str] = None
|
||||
|
||||
class RelatedProductCreate(RelatedProductBase):
|
||||
pass
|
||||
|
||||
class RelatedProductUpdate(BaseModel):
|
||||
relationship_type: Optional[str] = None
|
||||
|
||||
class RelatedProduct(RelatedProductBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Product with related products
|
||||
class ProductWithRelated(Product):
|
||||
related_products: List["Product"] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in New Issue
Block a user