Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
151 changes: 95 additions & 56 deletions backend/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
import psycopg
from dotenv import load_dotenv
import time
import logging

load_dotenv(dotenv_path="../.env")
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "../.env"), override=True)

def get_db_connection():
return psycopg.connect(
Expand All @@ -18,8 +22,10 @@ def init_db():
retries = 5
while retries > 0:
try:
logger.info("Attempting to connect to the database...")
with get_db_connection() as conn:
with conn.cursor() as cur:
logger.info("Connected to the database. Creating tables...")
cur.execute("""
DO $$
BEGIN
Expand All @@ -33,61 +39,94 @@ def init_db():
$$;
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS books (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
published_year INT NOT NULL,
isbn VARCHAR(20) UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE IF NOT EXISTS book_categories (
book_id INT REFERENCES books(id) ON DELETE CASCADE,
category_id INT REFERENCES categories(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, category_id)
);
CREATE TABLE IF NOT EXISTS authors (
id SERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS book_authors (
book_id INT REFERENCES books(id) ON DELETE CASCADE,
author_id INT REFERENCES authors(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, author_id)
);
CREATE TABLE IF NOT EXISTS book_copies (
id SERIAL PRIMARY KEY,
book_id INT REFERENCES books(id) ON DELETE CASCADE,
barcode VARCHAR(50) UNIQUE NOT NULL,
status book_status NOT NULL DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
fine DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
access_level access_level NOT NULL DEFAULT 'member',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS loans (
id SERIAL PRIMARY KEY,
copy_id INT REFERENCES book_copies(id) ON DELETE CASCADE,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
loan_date DATE NOT NULL DEFAULT CURRENT_DATE,
due_date DATE NOT NULL,
return_date DATE
);
""")
print("Database initialized successfully.")
break
CREATE TABLE IF NOT EXISTS books (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
published_year INT NOT NULL,
isbn VARCHAR(20) UNIQUE NOT NULL,
summary TEXT
);
CREATE TABLE IF NOT EXISTS categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE IF NOT EXISTS book_categories (
book_id INT REFERENCES books(id) ON DELETE CASCADE,
category_id INT REFERENCES categories(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, category_id)
);
CREATE TABLE IF NOT EXISTS authors (
id SERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS book_authors (
book_id INT REFERENCES books(id) ON DELETE CASCADE,
author_id INT REFERENCES authors(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, author_id)
);
CREATE TABLE IF NOT EXISTS book_copies (
id SERIAL PRIMARY KEY,
book_id INT REFERENCES books(id) ON DELETE CASCADE,
barcode VARCHAR(50) UNIQUE NOT NULL,
status book_status NOT NULL DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
fine DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
access_level access_level NOT NULL DEFAULT 'member',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS loans (
id SERIAL PRIMARY KEY,
copy_id INT REFERENCES book_copies(id) ON DELETE CASCADE,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
loan_date DATE NOT NULL DEFAULT CURRENT_DATE,
due_date DATE NOT NULL,
return_date DATE
);
""")
logger.info("Tables created successfully. Checking environment variables...")

test_user_vars = {
"TEST_USER_FIRST_NAME": os.getenv("TEST_USER_FIRST_NAME"),
"TEST_USER_LAST_NAME": os.getenv("TEST_USER_LAST_NAME"),
"TEST_USER_EMAIL": os.getenv("TEST_USER_EMAIL"),
"TEST_USER_PASSWORD": os.getenv("TEST_USER_PASSWORD"),
}
for var, value in test_user_vars.items():
logger.info(f"{var}: {value}")
if not all(test_user_vars.values()):
raise ValueError("Missing required environment variables for the test user.")
logger.info("Adding test user...")
cur.execute("""
INSERT INTO users (first_name, last_name, email, password, access_level)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (email) DO NOTHING;
""", (
test_user_vars["TEST_USER_FIRST_NAME"],
test_user_vars["TEST_USER_LAST_NAME"],
test_user_vars["TEST_USER_EMAIL"],
test_user_vars["TEST_USER_PASSWORD"],
"member"
))

if cur.rowcount > 0:
logger.info(f"Test user created successfully. Rows affected: {cur.rowcount}")
else:
logger.info("Test user already exists (skipped due to conflict).")

conn.commit()
logger.info("Database initialization completed.")
break
except Exception as e:
retries -= 1
print(f"Waiting for database... ({retries} retries left). Error: {e}")
logger.error(f"Database initialization failed: {e}")
logger.info(f"Retrying... ({retries} retries left)")
time.sleep(2)
13 changes: 12 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from fastapi import FastAPI
from database import get_db_connection, init_db
import threading
from tools.book_scraper import scrape_books
from routers import router_loans as loans

app = FastAPI(title="Library")
app.include_router(loans.router)

@app.on_event("startup")
def startup_event():
Expand All @@ -20,4 +24,11 @@ def health_check():

@app.get("/")
def root():
return {"message": "Library API is running"}
return {"message": "Library API is running"}

@app.post("/scrape-books")
def run_scraper(count: int = 10):
def scraper_thread():
scrape_books(count)
threading.Thread(target=scraper_thread, daemon=True).start()
return {"status": "started", "target_count": count}
3 changes: 3 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"deep-translator>=1.11.4",
"dotenv>=0.9.9",
"fastapi>=0.135.1",
"psycopg2-binary>=2.9.11",
"psycopg[binary]>=3.3.3",
"pytest>=9.0.3",
"python-dotenv>=1.2.2",
"qdrant-client>=1.17.1",
"requests>=2.33.1",
"uvicorn>=0.42.0",
]
111 changes: 111 additions & 0 deletions backend/routers/router_loans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from fastapi import APIRouter, HTTPException
from datetime import date
from schemas.schema_loans import Loan, LoanCreate
from database import get_db_connection
router = APIRouter(prefix="/loans", tags=["loans"])

@router.post("/", response_model=Loan)
async def create_loan(loan: LoanCreate):
try:
with get_db_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id FROM book_copies WHERE book_id = %s AND status = 'active' ORDER BY id LIMIT 1",
(loan.copy_id,)
)
copy = cur.fetchone()
if not copy:
raise HTTPException(status_code=404, detail="Book copy not found")

cur.execute("SELECT id FROM users WHERE id = %s", (loan.user_id,))
if not cur.fetchone():
raise HTTPException(status_code=404, detail="User not found")

cur.execute(
"""INSERT INTO loans (copy_id, user_id, due_date)
VALUES (%s, %s, %s)
RETURNING id, copy_id, user_id, loan_date, due_date, return_date""",
(loan.copy_id, loan.user_id, loan.due_date)
)
result = cur.fetchone()

cur.execute(
"UPDATE book_copies SET status = 'loaned' WHERE id = %s",
(loan.copy_id,)
)
conn.commit()

return {
"id": result[0],
"copy_id": result[1],
"user_id": result[2],
"loan_date": result[3],
"due_date": result[4],
"return_date": result[5]
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@router.post("/{loan_id}/return")
async def return_loan(loan_id: int):
try:
with get_db_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT copy_id, user_id, due_date FROM loans WHERE id = %s",
(loan_id,)
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="Loan not found")

copy_id, user_id, due_date = result
today = date.today()

if today > due_date:
days_overdue = (today - due_date).days
fine = days_overdue
else:
fine = 0

cur.execute(
"""UPDATE loans SET return_date = CURRENT_DATE
WHERE id = %s""",
(loan_id,)
)

cur.execute(
"""UPDATE users SET fine = fine + %s WHERE id = %s""",
(fine, user_id)
)

cur.execute(
"SELECT fine FROM users WHERE id = %s",
(user_id,)
)
total_fine = cur.fetchone()[0]

if total_fine > 20:
cur.execute(
"UPDATE users SET is_active = FALSE WHERE id = %s",
(user_id,)
)

cur.execute(
"UPDATE book_copies SET status = 'active' WHERE id = %s",
(copy_id,)
)
conn.commit()

return {
"message": "Book returned successfully",
"fine_added": fine,
"total_fine": total_fine,
"user_blocked": total_fine > 20
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Empty file added backend/schemas/__init__.py
Empty file.
File renamed without changes.
7 changes: 4 additions & 3 deletions backend/schemas/books.py → backend/schemas/schema_books.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from pydantic import BaseModel, field
from typing import List
from authors import Author
from categories import Category
from enums import BookStatus
from schema_authors import Author
from schema_categories import Category
from schema_enums import BookStatus

class BookCopyBase(BaseModel):
barcode: str
Expand All @@ -18,6 +18,7 @@ class BookBase(BaseModel):
title: str
published_year: int
isbn: str
summary: str

class BookCreate(BookBase):
author_ids: List[int]
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pydantic import BaseModel, EmailStr
from datetime import datetime
from decimal import Decimal
from enums import AccessLevel
from schema_enums import AccessLevel

class UserBase(BaseModel):
first_name: str
Expand Down
Empty file added backend/tests/__init__.py
Empty file.
Loading
Loading