mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Добавление catpcha при регистрации, фильтрация по количеству страниц
This commit is contained in:
@@ -8,6 +8,7 @@ from .books import router as books_router
|
||||
from .genres import router as genres_router
|
||||
from .loans import router as loans_router
|
||||
from .relationships import router as relationships_router
|
||||
from .cap import router as cap_router
|
||||
from .users import router as users_router
|
||||
from .misc import router as misc_router
|
||||
|
||||
@@ -22,5 +23,6 @@ api_router.include_router(authors_router, prefix="/api")
|
||||
api_router.include_router(books_router, prefix="/api")
|
||||
api_router.include_router(genres_router, prefix="/api")
|
||||
api_router.include_router(loans_router, prefix="/api")
|
||||
api_router.include_router(cap_router, prefix="/api")
|
||||
api_router.include_router(users_router, prefix="/api")
|
||||
api_router.include_router(relationships_router, prefix="/api")
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.services import require_captcha
|
||||
from library_service.models.db import Role, User
|
||||
from library_service.models.dto import (
|
||||
Token,
|
||||
@@ -63,7 +64,11 @@ router = APIRouter(prefix="/auth", tags=["authentication"])
|
||||
summary="Регистрация нового пользователя",
|
||||
description="Создает нового пользователя и возвращает резервные коды",
|
||||
)
|
||||
def register(user_data: UserCreate, session: Session = Depends(get_session)):
|
||||
def register(
|
||||
user_data: UserCreate,
|
||||
_=Depends(require_captcha),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Регистрирует нового пользователя в системе"""
|
||||
existing_user = session.exec(
|
||||
select(User).where(User.username == user_data.username)
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"""Модуль работы с авторами"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.auth import RequireStaff
|
||||
from library_service.settings import get_session
|
||||
from library_service.models.db import Author, AuthorBookLink, Book
|
||||
from library_service.models.dto import (BookRead, AuthorWithBooks,
|
||||
AuthorCreate, AuthorList, AuthorRead, AuthorUpdate)
|
||||
from library_service.models.dto import (
|
||||
BookRead,
|
||||
AuthorWithBooks,
|
||||
AuthorCreate,
|
||||
AuthorList,
|
||||
AuthorRead,
|
||||
AuthorUpdate,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/authors", tags=["authors"])
|
||||
@@ -59,7 +66,9 @@ def get_author(
|
||||
"""Возвращает информацию об авторе и его книгах"""
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found"
|
||||
)
|
||||
|
||||
books = session.exec(
|
||||
select(Book).join(AuthorBookLink).where(AuthorBookLink.author_id == author_id)
|
||||
@@ -88,7 +97,9 @@ def update_author(
|
||||
"""Обновляет информацию об авторе"""
|
||||
db_author = session.get(Author, author_id)
|
||||
if not db_author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found"
|
||||
)
|
||||
|
||||
update_data = author.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
@@ -113,7 +124,9 @@ def delete_author(
|
||||
"""Удаляет автора из системы"""
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found"
|
||||
)
|
||||
|
||||
author_read = AuthorRead(**author.model_dump())
|
||||
session.delete(author)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||
from sqlmodel import Session, select, col, func
|
||||
|
||||
from library_service.auth import RequireStaff
|
||||
@@ -56,10 +56,16 @@ def close_active_loan(session: Session, book_id: int) -> None:
|
||||
def filter_books(
|
||||
session: Session = Depends(get_session),
|
||||
q: str | None = Query(None, max_length=50, description="Поиск"),
|
||||
author_ids: List[int] | None = Query(None, description="Список ID авторов"),
|
||||
genre_ids: List[int] | None = Query(None, description="Список ID жанров"),
|
||||
min_page_count: int | None = Query(
|
||||
None, ge=0, description="Минимальное количество страниц"
|
||||
),
|
||||
max_page_count: int | None = Query(
|
||||
None, ge=0, description="Максимальное количество страниц"
|
||||
),
|
||||
author_ids: List[int] | None = Query(None, gt=0, description="Список ID авторов"),
|
||||
genre_ids: List[int] | None = Query(None, gt=0, description="Список ID жанров"),
|
||||
page: int = Query(1, gt=0, description="Номер страницы"),
|
||||
size: int = Query(20, gt=0, lt=101, description="Количество элементов на странице"),
|
||||
size: int = Query(20, gt=0, le=100, description="Количество элементов на странице"),
|
||||
):
|
||||
"""Возвращает отфильтрованный список книг с пагинацией"""
|
||||
statement = select(Book).distinct()
|
||||
@@ -69,6 +75,12 @@ def filter_books(
|
||||
(col(Book.title).ilike(f"%{q}%")) | (col(Book.description).ilike(f"%{q}%"))
|
||||
)
|
||||
|
||||
if min_page_count:
|
||||
statement = statement.where(Book.page_count >= min_page_count)
|
||||
|
||||
if max_page_count:
|
||||
statement = statement.where(Book.page_count <= max_page_count)
|
||||
|
||||
if author_ids:
|
||||
statement = statement.join(AuthorBookLink).where(
|
||||
AuthorBookLink.author_id.in_( # ty: ignore[unresolved-attribute, unresolved-reference]
|
||||
@@ -149,7 +161,9 @@ def get_book(
|
||||
"""Возвращает информацию о книге с авторами и жанрами"""
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
authors = session.exec(
|
||||
select(Author).join(AuthorBookLink).where(AuthorBookLink.book_id == book_id)
|
||||
@@ -185,12 +199,14 @@ def update_book(
|
||||
"""Обновляет информацию о книге"""
|
||||
db_book = session.get(Book, book_id)
|
||||
if not db_book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
if book_update.status is not None:
|
||||
if book_update.status == BookStatus.BORROWED:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Статус 'borrowed' устанавливается только через выдачу книги",
|
||||
)
|
||||
|
||||
@@ -226,7 +242,9 @@ def delete_book(
|
||||
"""Удаляет книгу из системы"""
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
book_read = BookRead(
|
||||
id=(book.id or 0),
|
||||
title=book.title,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from library_service.services.captcha import (
|
||||
limiter,
|
||||
get_ip,
|
||||
active_challenges,
|
||||
challenges_by_ip,
|
||||
MAX_CHALLENGES_PER_IP,
|
||||
MAX_TOTAL_CHALLENGES,
|
||||
CHALLENGE_TTL,
|
||||
REDEEM_TTL,
|
||||
prng,
|
||||
now_ms,
|
||||
redeem_tokens,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/cap", tags=["captcha"])
|
||||
|
||||
|
||||
@router.post("/challenge")
|
||||
@limiter.limit("15/minute")
|
||||
async def challenge(request: Request, ip: str = Depends(get_ip)):
|
||||
if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many challenges"
|
||||
)
|
||||
if len(active_challenges) >= MAX_TOTAL_CHALLENGES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Server busy"
|
||||
)
|
||||
|
||||
token = secrets.token_hex(25)
|
||||
redeem = secrets.token_hex(25)
|
||||
expires = now_ms() + CHALLENGE_TTL
|
||||
|
||||
active_challenges[token] = {
|
||||
"c": 50,
|
||||
"s": 32,
|
||||
"d": 4,
|
||||
"expires": expires,
|
||||
"redeem_token": redeem,
|
||||
"ip": ip,
|
||||
}
|
||||
challenges_by_ip[ip] += 1
|
||||
|
||||
return {"challenge": {"c": 50, "s": 32, "d": 4}, "token": token, "expires": expires}
|
||||
|
||||
|
||||
@router.post("/redeem")
|
||||
@limiter.limit("30/minute")
|
||||
async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
|
||||
token = payload.get("token")
|
||||
solutions = payload.get("solutions", [])
|
||||
|
||||
if token not in active_challenges:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Invalid challenge"
|
||||
)
|
||||
|
||||
ch = active_challenges.pop(token)
|
||||
challenges_by_ip[ch["ip"]] -= 1
|
||||
|
||||
if now_ms() > ch["expires"]:
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Expired")
|
||||
if len(solutions) < ch["c"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Bad solutions"
|
||||
)
|
||||
|
||||
def verify(i: int) -> bool:
|
||||
salt = prng(f"{token}{i+1}", ch["s"])
|
||||
target = prng(f"{token}{i+1}d", ch["d"])
|
||||
h = hashlib.sha256((salt + str(solutions[i])).encode()).hexdigest()
|
||||
return h.startswith(target)
|
||||
|
||||
results = await asyncio.gather(
|
||||
*(asyncio.to_thread(verify, i) for i in range(ch["c"]))
|
||||
)
|
||||
if not all(results):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid solution"
|
||||
)
|
||||
|
||||
r_token = ch["redeem_token"]
|
||||
redeem_tokens[r_token] = now_ms() + REDEEM_TTL
|
||||
|
||||
resp = JSONResponse(
|
||||
{"success": True, "token": r_token, "expires": redeem_tokens[r_token]}
|
||||
)
|
||||
resp.set_cookie(
|
||||
key="capjs_token",
|
||||
value=r_token,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=REDEEM_TTL // 1000,
|
||||
)
|
||||
return resp
|
||||
@@ -1,10 +1,18 @@
|
||||
"""Модуль работы с жанрами"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.auth import RequireStaff
|
||||
from library_service.models.db import Book, Genre, GenreBookLink
|
||||
from library_service.models.dto import BookRead, GenreCreate, GenreList, GenreRead, GenreUpdate, GenreWithBooks
|
||||
from library_service.models.dto import (
|
||||
BookRead,
|
||||
GenreCreate,
|
||||
GenreList,
|
||||
GenreRead,
|
||||
GenreUpdate,
|
||||
GenreWithBooks,
|
||||
)
|
||||
from library_service.settings import get_session
|
||||
|
||||
|
||||
@@ -57,7 +65,9 @@ def get_genre(
|
||||
"""Возвращает информацию о жанре и книгах с ним"""
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found"
|
||||
)
|
||||
|
||||
books = session.exec(
|
||||
select(Book).join(GenreBookLink).where(GenreBookLink.genre_id == genre_id)
|
||||
@@ -86,7 +96,9 @@ def update_genre(
|
||||
"""Обновляет информацию о жанре"""
|
||||
db_genre = session.get(Genre, genre_id)
|
||||
if not db_genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found"
|
||||
)
|
||||
|
||||
update_data = genre.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
@@ -111,7 +123,9 @@ def delete_genre(
|
||||
"""Удаляет жанр из системы"""
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found"
|
||||
)
|
||||
|
||||
genre_read = GenreRead(**genre.model_dump())
|
||||
session.delete(genre)
|
||||
|
||||
@@ -40,17 +40,21 @@ def create_loan(
|
||||
|
||||
book = session.get(Book, loan.book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
if book.status != BookStatus.ACTIVE:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Book is not available for loan (status: {book.status})",
|
||||
)
|
||||
|
||||
target_user = session.get(User, loan.user_id)
|
||||
if not target_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
|
||||
db_loan = BookUserLink(
|
||||
book_id=loan.book_id,
|
||||
@@ -248,7 +252,9 @@ def get_loan(
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
|
||||
if not loan:
|
||||
raise HTTPException(status_code=404, detail="Loan not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
||||
)
|
||||
|
||||
is_staff = is_user_staff(current_user)
|
||||
|
||||
@@ -275,7 +281,9 @@ def update_loan(
|
||||
"""Обновляет информацию о выдаче"""
|
||||
db_loan = session.get(BookUserLink, loan_id)
|
||||
if not db_loan:
|
||||
raise HTTPException(status_code=404, detail="Loan not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
||||
)
|
||||
|
||||
is_staff = is_user_staff(current_user)
|
||||
|
||||
@@ -287,7 +295,9 @@ def update_loan(
|
||||
|
||||
book = session.get(Book, db_loan.book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
if loan_update.user_id is not None:
|
||||
if not is_staff:
|
||||
@@ -297,7 +307,9 @@ def update_loan(
|
||||
)
|
||||
new_user = session.get(User, loan_update.user_id)
|
||||
if not new_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
db_loan.user_id = loan_update.user_id
|
||||
|
||||
if loan_update.due_date is not None:
|
||||
@@ -305,7 +317,10 @@ def update_loan(
|
||||
|
||||
if loan_update.returned_at is not None:
|
||||
if db_loan.returned_at is not None:
|
||||
raise HTTPException(status_code=400, detail="Loan is already returned")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Loan is already returned",
|
||||
)
|
||||
db_loan.returned_at = loan_update.returned_at
|
||||
book.status = BookStatus.ACTIVE
|
||||
|
||||
@@ -331,18 +346,24 @@ def confirm_loan(
|
||||
"""Подтверждает бронирование и меняет статус книги на BORROWED"""
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
if not loan:
|
||||
raise HTTPException(status_code=404, detail="Loan not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
||||
)
|
||||
|
||||
if loan.returned_at:
|
||||
raise HTTPException(status_code=400, detail="Loan is already returned")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Loan is already returned"
|
||||
)
|
||||
|
||||
book = session.get(Book, loan.book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
if book.status not in [BookStatus.RESERVED, BookStatus.ACTIVE]:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot confirm loan for book with status: {book.status}",
|
||||
)
|
||||
|
||||
@@ -370,10 +391,14 @@ def return_loan(
|
||||
"""Возвращает книгу и закрывает выдачу"""
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
if not loan:
|
||||
raise HTTPException(status_code=404, detail="Loan not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
||||
)
|
||||
|
||||
if loan.returned_at:
|
||||
raise HTTPException(status_code=400, detail="Loan is already returned")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Loan is already returned"
|
||||
)
|
||||
|
||||
loan.returned_at = datetime.now(timezone.utc)
|
||||
|
||||
@@ -403,7 +428,9 @@ def delete_loan(
|
||||
"""Удаляет выдачу или бронирование (только для RESERVED статуса)"""
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
if not loan:
|
||||
raise HTTPException(status_code=404, detail="Loan not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
||||
)
|
||||
|
||||
is_staff = is_user_staff(current_user)
|
||||
|
||||
@@ -417,7 +444,7 @@ def delete_loan(
|
||||
|
||||
if book and book.status != BookStatus.RESERVED:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Can only delete reservations. Use update endpoint to return borrowed books",
|
||||
)
|
||||
|
||||
@@ -471,16 +498,21 @@ def issue_book_directly(
|
||||
"""Выдает книгу напрямую без бронирования (только для администраторов)"""
|
||||
book = session.get(Book, loan.book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
if book.status != BookStatus.ACTIVE:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Book is not available (status: {book.status})"
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Book is not available (status: {book.status})",
|
||||
)
|
||||
|
||||
target_user = session.get(User, loan.user_id)
|
||||
if not target_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
|
||||
db_loan = BookUserLink(
|
||||
book_id=loan.book_id,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Модуль работы со связями"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.auth import RequireStaff
|
||||
@@ -17,7 +18,9 @@ def check_entity_exists(session, model, entity_id, entity_name):
|
||||
"""Проверяет существование сущности в базе данных"""
|
||||
entity = session.get(model, entity_id)
|
||||
if not entity:
|
||||
raise HTTPException(status_code=404, detail=f"{entity_name} not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"{entity_name} not found"
|
||||
)
|
||||
return entity
|
||||
|
||||
|
||||
@@ -30,7 +33,7 @@ def add_relationship(session, link_model, id1, field1, id2, field2, detail):
|
||||
).first()
|
||||
|
||||
if existing_link:
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
||||
|
||||
link = link_model(**{field1: id1, field2: id2})
|
||||
session.add(link)
|
||||
@@ -48,7 +51,9 @@ def remove_relationship(session, link_model, id1, field1, id2, field2):
|
||||
).first()
|
||||
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Relationship not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Relationship not found"
|
||||
)
|
||||
|
||||
session.delete(link)
|
||||
session.commit()
|
||||
@@ -56,21 +61,22 @@ def remove_relationship(session, link_model, id1, field1, id2, field2):
|
||||
|
||||
|
||||
def get_related(
|
||||
session,
|
||||
main_model,
|
||||
main_id,
|
||||
main_name,
|
||||
related_model,
|
||||
link_model,
|
||||
link_main_field,
|
||||
link_related_field,
|
||||
read_model
|
||||
):
|
||||
session,
|
||||
main_model,
|
||||
main_id,
|
||||
main_name,
|
||||
related_model,
|
||||
link_model,
|
||||
link_main_field,
|
||||
link_related_field,
|
||||
read_model,
|
||||
):
|
||||
"""Возвращает список связанных сущностей"""
|
||||
check_entity_exists(session, main_model, main_id, main_name)
|
||||
|
||||
related = session.exec(
|
||||
select(related_model).join(link_model)
|
||||
select(related_model)
|
||||
.join(link_model)
|
||||
.where(getattr(link_model, link_main_field) == main_id)
|
||||
).all()
|
||||
|
||||
@@ -93,8 +99,15 @@ def add_author_to_book(
|
||||
check_entity_exists(session, Author, author_id, "Author")
|
||||
check_entity_exists(session, Book, book_id, "Book")
|
||||
|
||||
return add_relationship(session, AuthorBookLink,
|
||||
author_id, "author_id", book_id, "book_id", "Relationship already exists")
|
||||
return add_relationship(
|
||||
session,
|
||||
AuthorBookLink,
|
||||
author_id,
|
||||
"author_id",
|
||||
book_id,
|
||||
"book_id",
|
||||
"Relationship already exists",
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -110,8 +123,9 @@ def remove_author_from_book(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Удаляет связь между автором и книгой"""
|
||||
return remove_relationship(session, AuthorBookLink,
|
||||
author_id, "author_id", book_id, "book_id")
|
||||
return remove_relationship(
|
||||
session, AuthorBookLink, author_id, "author_id", book_id, "book_id"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -122,9 +136,17 @@ def remove_author_from_book(
|
||||
)
|
||||
def get_books_for_author(author_id: int, session: Session = Depends(get_session)):
|
||||
"""Возвращает список книг автора"""
|
||||
return get_related(session,
|
||||
Author, author_id, "Author", Book,
|
||||
AuthorBookLink, "author_id", "book_id", BookRead)
|
||||
return get_related(
|
||||
session,
|
||||
Author,
|
||||
author_id,
|
||||
"Author",
|
||||
Book,
|
||||
AuthorBookLink,
|
||||
"author_id",
|
||||
"book_id",
|
||||
BookRead,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -135,9 +157,17 @@ def get_books_for_author(author_id: int, session: Session = Depends(get_session)
|
||||
)
|
||||
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||
"""Возвращает список авторов книги"""
|
||||
return get_related(session,
|
||||
Book, book_id, "Book", Author,
|
||||
AuthorBookLink, "book_id", "author_id", AuthorRead)
|
||||
return get_related(
|
||||
session,
|
||||
Book,
|
||||
book_id,
|
||||
"Book",
|
||||
Author,
|
||||
AuthorBookLink,
|
||||
"book_id",
|
||||
"author_id",
|
||||
AuthorRead,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -156,8 +186,15 @@ def add_genre_to_book(
|
||||
check_entity_exists(session, Genre, genre_id, "Genre")
|
||||
check_entity_exists(session, Book, book_id, "Book")
|
||||
|
||||
return add_relationship(session, GenreBookLink,
|
||||
genre_id, "genre_id", book_id, "book_id", "Relationship already exists")
|
||||
return add_relationship(
|
||||
session,
|
||||
GenreBookLink,
|
||||
genre_id,
|
||||
"genre_id",
|
||||
book_id,
|
||||
"book_id",
|
||||
"Relationship already exists",
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -173,8 +210,9 @@ def remove_genre_from_book(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Удаляет связь между жанром и книгой"""
|
||||
return remove_relationship(session, GenreBookLink,
|
||||
genre_id, "genre_id", book_id, "book_id")
|
||||
return remove_relationship(
|
||||
session, GenreBookLink, genre_id, "genre_id", book_id, "book_id"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -185,9 +223,17 @@ def remove_genre_from_book(
|
||||
)
|
||||
def get_books_for_genre(genre_id: int, session: Session = Depends(get_session)):
|
||||
"""Возвращает список книг в жанре"""
|
||||
return get_related(session,
|
||||
Genre, genre_id, "Genre", Book,
|
||||
GenreBookLink, "genre_id", "book_id", BookRead)
|
||||
return get_related(
|
||||
session,
|
||||
Genre,
|
||||
genre_id,
|
||||
"Genre",
|
||||
Book,
|
||||
GenreBookLink,
|
||||
"genre_id",
|
||||
"book_id",
|
||||
BookRead,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -198,6 +244,14 @@ def get_books_for_genre(genre_id: int, session: Session = Depends(get_session)):
|
||||
)
|
||||
def get_genres_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||
"""Возвращает список жанров книги"""
|
||||
return get_related(session,
|
||||
Book, book_id, "Book", Genre,
|
||||
GenreBookLink, "book_id", "genre_id", GenreRead)
|
||||
return get_related(
|
||||
session,
|
||||
Book,
|
||||
book_id,
|
||||
"Book",
|
||||
Genre,
|
||||
GenreBookLink,
|
||||
"book_id",
|
||||
"genre_id",
|
||||
GenreRead,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user