Spaces:
Sleeping
Sleeping
Upload 31 files
Browse files- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/database.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/crud/__init__.py +18 -0
- app/crud/__pycache__/__init__.cpython-312.pyc +0 -0
- app/crud/__pycache__/create.cpython-312.pyc +0 -0
- app/crud/__pycache__/delete.cpython-312.pyc +0 -0
- app/crud/__pycache__/read.cpython-312.pyc +0 -0
- app/crud/__pycache__/update.cpython-312.pyc +0 -0
- app/crud/create.py +29 -0
- app/crud/delete.py +48 -0
- app/crud/read.py +46 -0
- app/crud/update.py +48 -0
- app/database.py +24 -0
- app/main.py +98 -0
- app/models/__init__.py +0 -0
- app/models/__pycache__/__init__.cpython-312.pyc +0 -0
- app/models/__pycache__/item.cpython-312.pyc +0 -0
- app/models/item.py +10 -0
- app/routes/__init__.py +0 -0
- app/routes/__pycache__/__init__.cpython-312.pyc +0 -0
- app/routes/__pycache__/item.cpython-312.pyc +0 -0
- app/routes/item.py +50 -0
- app/schemas/__init__.py +0 -0
- app/schemas/__pycache__/__init__.cpython-312.pyc +0 -0
- app/schemas/__pycache__/item.cpython-312.pyc +0 -0
- app/schemas/item.py +16 -0
- app/static/css/styles.css +97 -0
- app/static/js/script.js +303 -0
- app/templates/index.html +126 -0
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (174 Bytes). View file
|
|
|
app/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (895 Bytes). View file
|
|
|
app/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (1.6 kB). View file
|
|
|
app/crud/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CRUD operations for the Item model.
|
| 3 |
+
This module imports and exposes all CRUD operations from their respective modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# Import operations
|
| 7 |
+
from app.crud.create import create_item
|
| 8 |
+
from app.crud.read import get_item, get_items
|
| 9 |
+
from app.crud.update import update_item
|
| 10 |
+
from app.crud.delete import delete_item
|
| 11 |
+
|
| 12 |
+
# Re-export all operations
|
| 13 |
+
__all__ = [
|
| 14 |
+
"create_item", # Create operations
|
| 15 |
+
"get_item", "get_items", # Read operations
|
| 16 |
+
"update_item", # Update operations
|
| 17 |
+
"delete_item", # Delete operations
|
| 18 |
+
]
|
app/crud/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (597 Bytes). View file
|
|
|
app/crud/__pycache__/create.cpython-312.pyc
ADDED
|
Binary file (806 Bytes). View file
|
|
|
app/crud/__pycache__/delete.cpython-312.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
app/crud/__pycache__/read.cpython-312.pyc
ADDED
|
Binary file (2.17 kB). View file
|
|
|
app/crud/__pycache__/update.cpython-312.pyc
ADDED
|
Binary file (1.25 kB). View file
|
|
|
app/crud/create.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from app.models.item import Item
|
| 3 |
+
from app.schemas.item import ItemCreate
|
| 4 |
+
|
| 5 |
+
def create_item(db: Session, item: ItemCreate):
|
| 6 |
+
db_item = Item(title=item.title,description=item.description,completed=item.completed)
|
| 7 |
+
db.add(db_item)
|
| 8 |
+
db.commit()
|
| 9 |
+
db.refresh(db_item)
|
| 10 |
+
return db_item
|
| 11 |
+
"""
|
| 12 |
+
Create a new item in the database.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
db (Session): Database session
|
| 16 |
+
item (ItemCreate): Item data to create
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
Item: The created item
|
| 20 |
+
"""
|
| 21 |
+
# TODO: Implement the create_item function
|
| 22 |
+
# The function should:
|
| 23 |
+
# 1. Create a new Item object with the data from item
|
| 24 |
+
# 2. Add the item to the database session
|
| 25 |
+
# 3. Commit the transaction
|
| 26 |
+
# 4. Refresh the item to get the generated ID
|
| 27 |
+
# 5. Return the created item
|
| 28 |
+
|
| 29 |
+
|
app/crud/delete.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from app.models.item import Item
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def delete_item(db: Session, item_id: int) -> bool:
|
| 7 |
+
"""
|
| 8 |
+
Delete an item from the database.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
db (Session): Database session
|
| 12 |
+
item_id (int): ID of the item to delete
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
bool: True if the item was deleted, False if the item was not found
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
item = db.query(Item).filter(Item.id == item_id).first()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if not item:
|
| 22 |
+
return False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
db.delete(item)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
db.commit()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
return True
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# TODO: Implement the delete_item function
|
| 39 |
+
# The function should:
|
| 40 |
+
# 1. Get the item with the given item_id from the database
|
| 41 |
+
# 2. If the item doesn't exist, return False
|
| 42 |
+
# 3. Delete the item from the database
|
| 43 |
+
# 4. Commit the changes
|
| 44 |
+
# 5. Return True to indicate successful deletion
|
| 45 |
+
|
| 46 |
+
# Delete the code below and implement your solution
|
| 47 |
+
|
| 48 |
+
|
app/crud/read.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from app.models.item import Item
|
| 4 |
+
|
| 5 |
+
def get_item(db: Session, item_id: int) -> Optional[Item]:
|
| 6 |
+
return db.query(Item).filter(Item.id == item_id).first()
|
| 7 |
+
|
| 8 |
+
def get_items(db: Session, skip: int = 0, limit: int = 100) -> List[Item]:
|
| 9 |
+
item_list = db.query(Item).offset(skip).limit(limit).all()
|
| 10 |
+
return item_list
|
| 11 |
+
|
| 12 |
+
from typing import List
|
| 13 |
+
from fastapi import Header, HTTPException
|
| 14 |
+
from sqlalchemy.orm import Session
|
| 15 |
+
|
| 16 |
+
API_KEY = "1234"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_items(
|
| 20 |
+
db: Session,
|
| 21 |
+
skip: int = 0,
|
| 22 |
+
limit: int = 100,
|
| 23 |
+
api_key: str = Header(..., alias="api_key")
|
| 24 |
+
) -> List[Item]:
|
| 25 |
+
"""
|
| 26 |
+
Get multiple items with pagination.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
db (Session): Database session
|
| 30 |
+
skip (int): Number of records to skip (for pagination)
|
| 31 |
+
limit (int): Maximum number of records to return
|
| 32 |
+
api_key (str): Required API key from request header
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
List[Item]: List of found items
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
# Validate API key
|
| 39 |
+
if api_key != API_KEY:
|
| 40 |
+
raise HTTPException(
|
| 41 |
+
status_code=401,
|
| 42 |
+
detail="Invalid API key"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Query all items with pagination
|
| 46 |
+
return db.query(Item).offset(skip).limit(limit).all()
|
app/crud/update.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from typing import Dict, Any, Optional
|
| 3 |
+
from app.models.item import Item
|
| 4 |
+
from app.schemas.item import ItemCreate
|
| 5 |
+
|
| 6 |
+
def update_item(db: Session, item_id: int, item: ItemCreate) -> Optional[Item]:
|
| 7 |
+
update_data = item.dict(exclude_unset=True)
|
| 8 |
+
result = db.query(Item).filter(Item.id == item_id).update(update_data)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
if result == 0:
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
db.commit()
|
| 17 |
+
updated_item = db.query(Item).filter(Item.id == item_id).first()
|
| 18 |
+
db.refresh(updated_item)
|
| 19 |
+
return updated_item
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
"""
|
| 26 |
+
Update an existing item in the database.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
db (Session): Database session
|
| 30 |
+
item_id (int): ID of the item to update
|
| 31 |
+
item (ItemCreate): New item data
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
Optional[Item]: The updated item or None if not found
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# TODO: Implement the update_item function
|
| 39 |
+
# The function should:
|
| 40 |
+
# 1. Get the item with the given item_id from the database
|
| 41 |
+
# 2. If the item doesn't exist, return None
|
| 42 |
+
# 3. Update the item's attributes with the new values
|
| 43 |
+
# 4. Commit the changes to the database
|
| 44 |
+
# 5. Refresh the item from the database
|
| 45 |
+
# 6. Return the updated item
|
| 46 |
+
|
| 47 |
+
# Delete the code below and implement your solution
|
| 48 |
+
|
app/database.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
|
| 5 |
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./database/items.db"
|
| 6 |
+
|
| 7 |
+
# Create SQLite engine
|
| 8 |
+
engine = create_engine(
|
| 9 |
+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
# Create SessionLocal class
|
| 13 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 14 |
+
|
| 15 |
+
# Create Base class
|
| 16 |
+
Base = declarative_base()
|
| 17 |
+
|
| 18 |
+
# Dependency
|
| 19 |
+
def get_db():
|
| 20 |
+
db = SessionLocal()
|
| 21 |
+
try:
|
| 22 |
+
yield db
|
| 23 |
+
finally:
|
| 24 |
+
db.close()
|
app/main.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from fastapi import FastAPI, Request
|
| 3 |
+
from fastapi.responses import HTMLResponse
|
| 4 |
+
from fastapi.staticfiles import StaticFiles
|
| 5 |
+
from fastapi.templating import Jinja2Templates
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
from app.database import engine
|
| 9 |
+
from app.models.item import Item
|
| 10 |
+
import app.routes.item as item_routes
|
| 11 |
+
|
| 12 |
+
# Create tables in the database
|
| 13 |
+
from app.database import Base
|
| 14 |
+
Base.metadata.create_all(bind=engine)
|
| 15 |
+
|
| 16 |
+
# Initialize FastAPI app
|
| 17 |
+
app = FastAPI(title="FastAPI CRUD App")
|
| 18 |
+
|
| 19 |
+
# Mount static files
|
| 20 |
+
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
| 21 |
+
|
| 22 |
+
# Set up templates
|
| 23 |
+
templates = Jinja2Templates(directory="app/templates")
|
| 24 |
+
|
| 25 |
+
# Include routers
|
| 26 |
+
app.include_router(item_routes.router)
|
| 27 |
+
|
| 28 |
+
# Root endpoint
|
| 29 |
+
@app.get("/", response_class=HTMLResponse)
|
| 30 |
+
async def read_root(request: Request):
|
| 31 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 32 |
+
|
| 33 |
+
# Run with: uvicorn app.main:app --reload
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
import uvicorn
|
| 36 |
+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
| 37 |
+
|
| 38 |
+
"""
|
| 39 |
+
# IGNORE THE FOLLOWING SECTION
|
| 40 |
+
from fastapi import FastAPI, Request
|
| 41 |
+
from fastapi.responses import HTMLResponse
|
| 42 |
+
from fastapi.staticfiles import StaticFiles
|
| 43 |
+
from fastapi.templating import Jinja2Templates
|
| 44 |
+
import os
|
| 45 |
+
import sys
|
| 46 |
+
import importlib
|
| 47 |
+
|
| 48 |
+
from app.database import engine
|
| 49 |
+
from app.models.item import Item
|
| 50 |
+
|
| 51 |
+
# Create tables in the database
|
| 52 |
+
from app.database import Base
|
| 53 |
+
Base.metadata.create_all(bind=engine)
|
| 54 |
+
|
| 55 |
+
# Initialize FastAPI app
|
| 56 |
+
app = FastAPI(title="FastAPI CRUD App")
|
| 57 |
+
|
| 58 |
+
# Mount static files
|
| 59 |
+
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
| 60 |
+
|
| 61 |
+
# Set up templates
|
| 62 |
+
templates = Jinja2Templates(directory="app/templates")
|
| 63 |
+
|
| 64 |
+
# TEMP: Use the solutions for testing
|
| 65 |
+
# Import CRUD operations from solutions directory
|
| 66 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 67 |
+
from solutions.crud.create import create_item
|
| 68 |
+
from solutions.crud.read import get_item, get_items
|
| 69 |
+
from solutions.crud.update import update_item
|
| 70 |
+
from solutions.crud.delete import delete_item
|
| 71 |
+
|
| 72 |
+
# Create a temporary crud module that uses solutions
|
| 73 |
+
class TempCrud:
|
| 74 |
+
create_item = create_item
|
| 75 |
+
get_item = get_item
|
| 76 |
+
get_items = get_items
|
| 77 |
+
update_item = update_item
|
| 78 |
+
delete_item = delete_item
|
| 79 |
+
|
| 80 |
+
# Import the router
|
| 81 |
+
import app.routes.item as item_routes
|
| 82 |
+
|
| 83 |
+
# Replace the crud import in the routes with our temporary one
|
| 84 |
+
item_routes.crud = TempCrud
|
| 85 |
+
|
| 86 |
+
# Include routers
|
| 87 |
+
app.include_router(item_routes.router)
|
| 88 |
+
|
| 89 |
+
# Root endpoint
|
| 90 |
+
@app.get("/", response_class=HTMLResponse)
|
| 91 |
+
async def read_root(request: Request):
|
| 92 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 93 |
+
|
| 94 |
+
# Run with: uvicorn app.main:app --reload
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
import uvicorn
|
| 97 |
+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
| 98 |
+
"""
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (181 Bytes). View file
|
|
|
app/models/__pycache__/item.cpython-312.pyc
ADDED
|
Binary file (742 Bytes). View file
|
|
|
app/models/item.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Boolean
|
| 2 |
+
from app.database import Base
|
| 3 |
+
|
| 4 |
+
class Item(Base):
|
| 5 |
+
__tablename__ = "items"
|
| 6 |
+
|
| 7 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 8 |
+
title = Column(String, index=True)
|
| 9 |
+
description = Column(String)
|
| 10 |
+
completed = Column(Boolean, default=False)
|
app/routes/__init__.py
ADDED
|
File without changes
|
app/routes/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (181 Bytes). View file
|
|
|
app/routes/__pycache__/item.cpython-312.pyc
ADDED
|
Binary file (2.97 kB). View file
|
|
|
app/routes/item.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
from app.database import get_db
|
| 6 |
+
from app.schemas.item import Item, ItemCreate
|
| 7 |
+
import app.crud as crud
|
| 8 |
+
|
| 9 |
+
router = APIRouter(
|
| 10 |
+
prefix="/api/items",
|
| 11 |
+
tags=["items"],
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
# CREATE operation
|
| 15 |
+
@router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED)
|
| 16 |
+
def create_item(item: ItemCreate, db: Session = Depends(get_db)):
|
| 17 |
+
"""Create a new item"""
|
| 18 |
+
return crud.create_item(db=db, item=item)
|
| 19 |
+
|
| 20 |
+
from fastapi import Header
|
| 21 |
+
@router.get("/", response_model=List[Item])
|
| 22 |
+
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), api_key: str = Header(..., alias="api_key")):
|
| 23 |
+
"""Get all items with pagination"""
|
| 24 |
+
return crud.get_items(db=db, skip=skip, limit=limit, api_key=api_key)
|
| 25 |
+
|
| 26 |
+
@router.get("/{item_id}", response_model=Item)
|
| 27 |
+
def read_item(item_id: int, db: Session = Depends(get_db)):
|
| 28 |
+
"""Get a specific item by ID"""
|
| 29 |
+
db_item = crud.get_item(db=db, item_id=item_id)
|
| 30 |
+
if db_item is None:
|
| 31 |
+
raise HTTPException(status_code=404, detail="Item not found")
|
| 32 |
+
return db_item
|
| 33 |
+
|
| 34 |
+
# UPDATE operation
|
| 35 |
+
@router.put("/{item_id}", response_model=Item)
|
| 36 |
+
def update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):
|
| 37 |
+
"""Update an existing item"""
|
| 38 |
+
db_item = crud.update_item(db=db, item_id=item_id, item=item)
|
| 39 |
+
if db_item is None:
|
| 40 |
+
raise HTTPException(status_code=404, detail="Item not found")
|
| 41 |
+
return db_item
|
| 42 |
+
|
| 43 |
+
# DELETE operation
|
| 44 |
+
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 45 |
+
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
| 46 |
+
"""Delete a specific item"""
|
| 47 |
+
success = crud.delete_item(db=db, item_id=item_id)
|
| 48 |
+
if not success:
|
| 49 |
+
raise HTTPException(status_code=404, detail="Item not found")
|
| 50 |
+
return None
|
app/schemas/__init__.py
ADDED
|
File without changes
|
app/schemas/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (182 Bytes). View file
|
|
|
app/schemas/__pycache__/item.cpython-312.pyc
ADDED
|
Binary file (1.11 kB). View file
|
|
|
app/schemas/item.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
class ItemBase(BaseModel):
|
| 5 |
+
title: str
|
| 6 |
+
description: Optional[str] = None
|
| 7 |
+
completed: bool = False
|
| 8 |
+
|
| 9 |
+
class ItemCreate(ItemBase):
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
class Item(ItemBase):
|
| 13 |
+
id: int
|
| 14 |
+
|
| 15 |
+
class Config:
|
| 16 |
+
from_attributes = True
|
app/static/css/styles.css
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Custom styles for the CRUD application */
|
| 2 |
+
|
| 3 |
+
body {
|
| 4 |
+
background-color: #f8f9fa;
|
| 5 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
header {
|
| 9 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.card {
|
| 13 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 14 |
+
border: none;
|
| 15 |
+
transition: transform 0.3s;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.card:hover {
|
| 19 |
+
transform: translateY(-5px);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.card-header {
|
| 23 |
+
font-weight: 600;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.btn-primary {
|
| 27 |
+
background-color: #0d6efd;
|
| 28 |
+
border-color: #0d6efd;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.btn-primary:hover {
|
| 32 |
+
background-color: #0b5ed7;
|
| 33 |
+
border-color: #0a58ca;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.table th {
|
| 37 |
+
background-color: #f1f3f5;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.badge-status {
|
| 41 |
+
font-size: 0.8rem;
|
| 42 |
+
padding: 0.35em 0.65em;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.completed-true {
|
| 46 |
+
background-color: #198754;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.completed-false {
|
| 50 |
+
background-color: #dc3545;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.action-btn {
|
| 54 |
+
transition: all 0.2s;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.action-btn:hover {
|
| 58 |
+
transform: scale(1.2);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.edit-btn {
|
| 62 |
+
color: #0d6efd;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.delete-btn {
|
| 66 |
+
color: #dc3545;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#no-items-message {
|
| 70 |
+
padding: 2rem;
|
| 71 |
+
background-color: #f8f9fa;
|
| 72 |
+
border-radius: 0.5rem;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* Toast customization */
|
| 76 |
+
.toast-container {
|
| 77 |
+
z-index: 11;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.toast {
|
| 81 |
+
opacity: 1;
|
| 82 |
+
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* Animation for new/updated items */
|
| 86 |
+
@keyframes highlight {
|
| 87 |
+
0% {
|
| 88 |
+
background-color: #e3f2fd;
|
| 89 |
+
}
|
| 90 |
+
100% {
|
| 91 |
+
background-color: transparent;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.highlight-row {
|
| 96 |
+
animation: highlight 2s ease-in-out;
|
| 97 |
+
}
|
app/static/js/script.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// DOM Elements
|
| 2 |
+
const itemForm = document.getElementById('item-form');
|
| 3 |
+
const formTitle = document.getElementById('form-title');
|
| 4 |
+
const itemIdInput = document.getElementById('item-id');
|
| 5 |
+
const titleInput = document.getElementById('title');
|
| 6 |
+
const descriptionInput = document.getElementById('description');
|
| 7 |
+
const completedCheckbox = document.getElementById('completed');
|
| 8 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 9 |
+
const cancelBtn = document.getElementById('cancel-btn');
|
| 10 |
+
const itemsTableBody = document.getElementById('items-table-body');
|
| 11 |
+
const noItemsMessage = document.getElementById('no-items-message');
|
| 12 |
+
const createFirstItemBtn = document.getElementById('create-first-item-btn');
|
| 13 |
+
const loadingSpinner = document.getElementById('loading-spinner');
|
| 14 |
+
const toastElement = document.getElementById('toast');
|
| 15 |
+
const toastTitle = document.getElementById('toast-title');
|
| 16 |
+
const toastMessage = document.getElementById('toast-message');
|
| 17 |
+
const toastIcon = document.getElementById('toast-icon');
|
| 18 |
+
|
| 19 |
+
// Create Bootstrap toast instance
|
| 20 |
+
const toast = new bootstrap.Toast(toastElement, {
|
| 21 |
+
delay: 3000
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// API Endpoints
|
| 25 |
+
const API_URL = '/api/items';
|
| 26 |
+
|
| 27 |
+
// App State
|
| 28 |
+
let isEditing = false;
|
| 29 |
+
let items = [];
|
| 30 |
+
|
| 31 |
+
// Event Listeners
|
| 32 |
+
document.addEventListener('DOMContentLoaded', fetchItems);
|
| 33 |
+
itemForm.addEventListener('submit', handleFormSubmit);
|
| 34 |
+
cancelBtn.addEventListener('click', resetForm);
|
| 35 |
+
if (createFirstItemBtn) {
|
| 36 |
+
createFirstItemBtn.addEventListener('click', () => {
|
| 37 |
+
window.scrollTo({
|
| 38 |
+
top: itemForm.offsetTop - 100,
|
| 39 |
+
behavior: 'smooth'
|
| 40 |
+
});
|
| 41 |
+
titleInput.focus();
|
| 42 |
+
});
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Functions
|
| 46 |
+
async function fetchItems() {
|
| 47 |
+
try {
|
| 48 |
+
showLoading(true);
|
| 49 |
+
const response = await fetch(API_URL);
|
| 50 |
+
const data = await response.json();
|
| 51 |
+
|
| 52 |
+
// Keep a reference to all items
|
| 53 |
+
items = data;
|
| 54 |
+
|
| 55 |
+
renderItems(items);
|
| 56 |
+
showLoading(false);
|
| 57 |
+
} catch (error) {
|
| 58 |
+
console.error('Error fetching items:', error);
|
| 59 |
+
showToast('Error', 'Failed to load items. Please try again.', 'error');
|
| 60 |
+
showLoading(false);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function renderItems(items) {
|
| 65 |
+
itemsTableBody.innerHTML = '';
|
| 66 |
+
|
| 67 |
+
if (items.length === 0) {
|
| 68 |
+
noItemsMessage.style.display = 'block';
|
| 69 |
+
document.querySelector('.table-responsive').style.display = 'none';
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
noItemsMessage.style.display = 'none';
|
| 74 |
+
document.querySelector('.table-responsive').style.display = 'block';
|
| 75 |
+
|
| 76 |
+
items.forEach(item => {
|
| 77 |
+
const row = document.createElement('tr');
|
| 78 |
+
row.id = `item-row-${item.id}`;
|
| 79 |
+
|
| 80 |
+
row.innerHTML = `
|
| 81 |
+
<td>${item.id}</td>
|
| 82 |
+
<td class="fw-medium">${escapeHtml(item.title)}</td>
|
| 83 |
+
<td>${escapeHtml(item.description || '-')}</td>
|
| 84 |
+
<td>
|
| 85 |
+
<span class="badge completed-${item.completed}">
|
| 86 |
+
${item.completed ?
|
| 87 |
+
'<i class="bi bi-check-circle me-1"></i>Completed' :
|
| 88 |
+
'<i class="bi bi-clock me-1"></i>Pending'}
|
| 89 |
+
</span>
|
| 90 |
+
</td>
|
| 91 |
+
<td class="text-center">
|
| 92 |
+
<div class="d-flex justify-content-center">
|
| 93 |
+
<button class="action-btn edit-btn" data-id="${item.id}" title="Edit Item">
|
| 94 |
+
<i class="bi bi-pencil-square"></i>
|
| 95 |
+
</button>
|
| 96 |
+
<button class="action-btn delete-btn" data-id="${item.id}" title="Delete Item">
|
| 97 |
+
<i class="bi bi-trash"></i>
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
+
</td>
|
| 101 |
+
`;
|
| 102 |
+
|
| 103 |
+
itemsTableBody.appendChild(row);
|
| 104 |
+
|
| 105 |
+
// Add event listeners to buttons
|
| 106 |
+
const editBtn = row.querySelector('.edit-btn');
|
| 107 |
+
const deleteBtn = row.querySelector('.delete-btn');
|
| 108 |
+
|
| 109 |
+
editBtn.addEventListener('click', () => editItem(item));
|
| 110 |
+
deleteBtn.addEventListener('click', () => deleteItem(item.id));
|
| 111 |
+
});
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
async function handleFormSubmit(event) {
|
| 115 |
+
event.preventDefault();
|
| 116 |
+
|
| 117 |
+
const itemData = {
|
| 118 |
+
title: titleInput.value.trim(),
|
| 119 |
+
description: descriptionInput.value.trim(),
|
| 120 |
+
completed: completedCheckbox.checked
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
if (!itemData.title) {
|
| 124 |
+
showToast('Validation Error', 'Title is required', 'error');
|
| 125 |
+
titleInput.focus();
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
try {
|
| 130 |
+
showLoading(true);
|
| 131 |
+
|
| 132 |
+
if (isEditing) {
|
| 133 |
+
// Update existing item
|
| 134 |
+
const itemId = parseInt(itemIdInput.value);
|
| 135 |
+
await updateItem(itemId, itemData);
|
| 136 |
+
showToast('Success', 'Item updated successfully!', 'success');
|
| 137 |
+
} else {
|
| 138 |
+
// Create new item
|
| 139 |
+
await createItem(itemData);
|
| 140 |
+
showToast('Success', 'New item created successfully!', 'success');
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
resetForm();
|
| 144 |
+
fetchItems();
|
| 145 |
+
} catch (error) {
|
| 146 |
+
console.error('Error saving item:', error);
|
| 147 |
+
showToast('Error', 'Failed to save item. Please try again.', 'error');
|
| 148 |
+
showLoading(false);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
async function createItem(itemData) {
|
| 153 |
+
const response = await fetch(API_URL, {
|
| 154 |
+
method: 'POST',
|
| 155 |
+
headers: {
|
| 156 |
+
'Content-Type': 'application/json'
|
| 157 |
+
},
|
| 158 |
+
body: JSON.stringify(itemData)
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
if (!response.ok) {
|
| 162 |
+
const error = await response.json();
|
| 163 |
+
throw new Error(error.detail || 'Failed to create item');
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
return await response.json();
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
async function updateItem(itemId, itemData) {
|
| 170 |
+
const response = await fetch(`${API_URL}/${itemId}`, {
|
| 171 |
+
method: 'PUT',
|
| 172 |
+
headers: {
|
| 173 |
+
'Content-Type': 'application/json'
|
| 174 |
+
},
|
| 175 |
+
body: JSON.stringify(itemData)
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
if (!response.ok) {
|
| 179 |
+
const error = await response.json();
|
| 180 |
+
throw new Error(error.detail || 'Failed to update item');
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
return await response.json();
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
async function deleteItem(itemId) {
|
| 187 |
+
// Create custom confirm dialog
|
| 188 |
+
if (!confirm('Are you sure you want to delete this item? This action cannot be undone.')) {
|
| 189 |
+
return;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
try {
|
| 193 |
+
showLoading(true);
|
| 194 |
+
|
| 195 |
+
const response = await fetch(`${API_URL}/${itemId}`, {
|
| 196 |
+
method: 'DELETE'
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
if (!response.ok) {
|
| 200 |
+
const error = await response.json();
|
| 201 |
+
throw new Error(error.detail || 'Failed to delete item');
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
showToast('Success', 'Item deleted successfully!', 'success');
|
| 205 |
+
|
| 206 |
+
// Remove item from local array
|
| 207 |
+
items = items.filter(item => item.id !== itemId);
|
| 208 |
+
|
| 209 |
+
// Re-render items
|
| 210 |
+
renderItems(items);
|
| 211 |
+
showLoading(false);
|
| 212 |
+
} catch (error) {
|
| 213 |
+
console.error('Error deleting item:', error);
|
| 214 |
+
showToast('Error', 'Failed to delete item. Please try again.', 'error');
|
| 215 |
+
showLoading(false);
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function editItem(item) {
|
| 220 |
+
// Set form to edit mode
|
| 221 |
+
isEditing = true;
|
| 222 |
+
formTitle.innerHTML = '<i class="bi bi-pencil-square me-2"></i>Edit Item';
|
| 223 |
+
submitBtn.innerHTML = '<i class="bi bi-save me-2"></i>Update';
|
| 224 |
+
cancelBtn.style.display = 'block';
|
| 225 |
+
|
| 226 |
+
// Populate form with item data
|
| 227 |
+
itemIdInput.value = item.id;
|
| 228 |
+
titleInput.value = item.title;
|
| 229 |
+
descriptionInput.value = item.description || '';
|
| 230 |
+
completedCheckbox.checked = item.completed;
|
| 231 |
+
|
| 232 |
+
// Scroll to form
|
| 233 |
+
window.scrollTo({
|
| 234 |
+
top: itemForm.offsetTop - 100,
|
| 235 |
+
behavior: 'smooth'
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
// Add highlight class to row
|
| 239 |
+
const row = document.getElementById(`item-row-${item.id}`);
|
| 240 |
+
if (row) {
|
| 241 |
+
row.classList.add('highlight-row');
|
| 242 |
+
setTimeout(() => {
|
| 243 |
+
row.classList.remove('highlight-row');
|
| 244 |
+
}, 2000);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// Focus on title input
|
| 248 |
+
titleInput.focus();
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
function resetForm() {
|
| 252 |
+
// Reset form state
|
| 253 |
+
isEditing = false;
|
| 254 |
+
formTitle.innerHTML = '<i class="bi bi-plus-circle me-2"></i>Add New Item';
|
| 255 |
+
submitBtn.innerHTML = '<i class="bi bi-save me-2"></i>Save';
|
| 256 |
+
cancelBtn.style.display = 'none';
|
| 257 |
+
|
| 258 |
+
// Clear form inputs
|
| 259 |
+
itemForm.reset();
|
| 260 |
+
itemIdInput.value = '';
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function showToast(title, message, type = 'info') {
|
| 264 |
+
toastTitle.textContent = title;
|
| 265 |
+
toastMessage.textContent = message;
|
| 266 |
+
|
| 267 |
+
// Remove any existing classes
|
| 268 |
+
toastElement.classList.remove('bg-success', 'bg-danger', 'bg-info', 'bg-warning', 'text-white');
|
| 269 |
+
toastIcon.classList.remove('bi-check-circle-fill', 'bi-exclamation-triangle-fill', 'bi-info-circle-fill');
|
| 270 |
+
|
| 271 |
+
// Add appropriate styling based on type
|
| 272 |
+
if (type === 'success') {
|
| 273 |
+
toastElement.classList.add('bg-success', 'text-white');
|
| 274 |
+
toastIcon.classList.add('bi-check-circle-fill');
|
| 275 |
+
} else if (type === 'error') {
|
| 276 |
+
toastElement.classList.add('bg-danger', 'text-white');
|
| 277 |
+
toastIcon.classList.add('bi-exclamation-triangle-fill');
|
| 278 |
+
} else {
|
| 279 |
+
toastElement.classList.add('bg-info', 'text-white');
|
| 280 |
+
toastIcon.classList.add('bi-info-circle-fill');
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
toast.show();
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function showLoading(isLoading) {
|
| 287 |
+
if (isLoading) {
|
| 288 |
+
loadingSpinner.style.display = 'inline-block';
|
| 289 |
+
} else {
|
| 290 |
+
loadingSpinner.style.display = 'none';
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Utility function to prevent XSS
|
| 295 |
+
function escapeHtml(str) {
|
| 296 |
+
if (!str) return '';
|
| 297 |
+
return str
|
| 298 |
+
.replace(/&/g, '&')
|
| 299 |
+
.replace(/</g, '<')
|
| 300 |
+
.replace(/>/g, '>')
|
| 301 |
+
.replace(/"/g, '"')
|
| 302 |
+
.replace(/'/g, ''');
|
| 303 |
+
}
|
app/templates/index.html
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>FastAPI CRUD App</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 12 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/styles.css') }}">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<header class="text-center py-4 mb-5">
|
| 16 |
+
<div class="container">
|
| 17 |
+
<h1 class="display-4 fw-bold">FastAPI CRUD Application</h1>
|
| 18 |
+
<p class="lead mb-0">Create, Read, Update, and Delete Items</p>
|
| 19 |
+
</div>
|
| 20 |
+
</header>
|
| 21 |
+
|
| 22 |
+
<main class="container mb-5">
|
| 23 |
+
<div class="row g-4">
|
| 24 |
+
<!-- Form section -->
|
| 25 |
+
<div class="col-lg-4 col-md-5">
|
| 26 |
+
<div class="card h-100">
|
| 27 |
+
<div class="card-header d-flex align-items-center">
|
| 28 |
+
<i class="bi bi-plus-circle me-2"></i>
|
| 29 |
+
<h2 class="h5 mb-0" id="form-title">Add New Item</h2>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="card-body">
|
| 32 |
+
<form id="item-form">
|
| 33 |
+
<input type="hidden" id="item-id">
|
| 34 |
+
<div class="mb-4">
|
| 35 |
+
<label for="title" class="form-label">Title</label>
|
| 36 |
+
<input type="text" class="form-control" id="title" placeholder="Enter item title" required>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="mb-4">
|
| 39 |
+
<label for="description" class="form-label">Description</label>
|
| 40 |
+
<textarea class="form-control" id="description" rows="4" placeholder="Enter item description"></textarea>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="mb-4 form-check">
|
| 43 |
+
<input type="checkbox" class="form-check-input" id="completed">
|
| 44 |
+
<label class="form-check-label" for="completed">Mark as completed</label>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
|
| 47 |
+
<button type="submit" class="btn btn-primary px-4" id="submit-btn">
|
| 48 |
+
<i class="bi bi-save me-2"></i>Save
|
| 49 |
+
</button>
|
| 50 |
+
<button type="button" class="btn btn-secondary px-4" id="cancel-btn" style="display: none;">
|
| 51 |
+
<i class="bi bi-x-circle me-2"></i>Cancel
|
| 52 |
+
</button>
|
| 53 |
+
</div>
|
| 54 |
+
</form>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<!-- Items list section -->
|
| 60 |
+
<div class="col-lg-8 col-md-7">
|
| 61 |
+
<div class="card h-100">
|
| 62 |
+
<div class="card-header d-flex justify-content-between align-items-center">
|
| 63 |
+
<div class="d-flex align-items-center">
|
| 64 |
+
<i class="bi bi-list-ul me-2"></i>
|
| 65 |
+
<h2 class="h5 mb-0">Items List</h2>
|
| 66 |
+
</div>
|
| 67 |
+
<div class="spinner-border text-light spinner-border-sm" role="status" id="loading-spinner">
|
| 68 |
+
<span class="visually-hidden">Loading...</span>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="card-body">
|
| 72 |
+
<div class="table-responsive">
|
| 73 |
+
<table class="table table-hover">
|
| 74 |
+
<thead>
|
| 75 |
+
<tr>
|
| 76 |
+
<th scope="col" width="5%">ID</th>
|
| 77 |
+
<th scope="col" width="25%">Title</th>
|
| 78 |
+
<th scope="col" width="40%">Description</th>
|
| 79 |
+
<th scope="col" width="15%">Status</th>
|
| 80 |
+
<th scope="col" width="15%" class="text-center">Actions</th>
|
| 81 |
+
</tr>
|
| 82 |
+
</thead>
|
| 83 |
+
<tbody id="items-table-body">
|
| 84 |
+
<!-- Items will be loaded here -->
|
| 85 |
+
</tbody>
|
| 86 |
+
</table>
|
| 87 |
+
</div>
|
| 88 |
+
<div id="no-items-message" style="display: none;">
|
| 89 |
+
<i class="bi bi-inbox text-muted d-block text-center" style="font-size: 3rem;"></i>
|
| 90 |
+
<p class="text-center mt-3">No items found. Create a new item to get started.</p>
|
| 91 |
+
<div class="text-center">
|
| 92 |
+
<button class="btn btn-primary" id="create-first-item-btn">
|
| 93 |
+
<i class="bi bi-plus-circle me-2"></i>Create First Item
|
| 94 |
+
</button>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</main>
|
| 102 |
+
|
| 103 |
+
<!-- Toast notifications -->
|
| 104 |
+
<div class="toast-container position-fixed top-0 end-0 p-3">
|
| 105 |
+
<div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
| 106 |
+
<div class="toast-header">
|
| 107 |
+
<i class="bi me-2" id="toast-icon"></i>
|
| 108 |
+
<strong class="me-auto" id="toast-title">Notification</strong>
|
| 109 |
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
| 110 |
+
</div>
|
| 111 |
+
<div class="toast-body" id="toast-message">
|
| 112 |
+
<!-- Toast message will be inserted here -->
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<footer class="py-4">
|
| 118 |
+
<div class="container">
|
| 119 |
+
<p>FastAPI CRUD Application © 2025 | Built with <i class="bi bi-heart-fill text-danger"></i> using FastAPI & SQLite</p>
|
| 120 |
+
</div>
|
| 121 |
+
</footer>
|
| 122 |
+
|
| 123 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
|
| 124 |
+
<script src="{{ url_for('static', path='/js/script.js') }}"></script>
|
| 125 |
+
</body>
|
| 126 |
+
</html>
|