diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py index 177dc1e476..7dfac1a6ea 100644 --- a/backend/app/api/routes/items.py +++ b/backend/app/api/routes/items.py @@ -2,7 +2,7 @@ from typing import Any from fastapi import APIRouter, HTTPException -from sqlmodel import func, select +from sqlmodel import func, select, or_ from app.api.deps import CurrentUser, SessionDep from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message @@ -12,31 +12,36 @@ @router.get("/", response_model=ItemsPublic) def read_items( - session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 + session: SessionDep, + current_user: CurrentUser, + skip: int = 0, + limit: int = 100, + search: str | None = None ) -> Any: """ Retrieve items. """ if current_user.is_superuser: + query = select(Item) count_statement = select(func.count()).select_from(Item) - count = session.exec(count_statement).one() - statement = select(Item).offset(skip).limit(limit) - items = session.exec(statement).all() else: + query = select(Item).where(Item.owner_id == current_user.id) count_statement = ( select(func.count()) .select_from(Item) .where(Item.owner_id == current_user.id) ) - count = session.exec(count_statement).one() - statement = ( - select(Item) - .where(Item.owner_id == current_user.id) - .offset(skip) - .limit(limit) + + if search: + search_filter = or_( + Item.title.contains(search, autoescape=True), # type: ignore + Item.description.contains(search, autoescape=True), # type: ignore ) - items = session.exec(statement).all() + query = query.where(search_filter) + count_statement = count_statement.where(search_filter) + count = session.exec(count_statement).one() + items = session.exec(query.offset(skip).limit(limit)).all() return ItemsPublic(data=items, count=count) diff --git a/backend/app/tests/api/routes/test_items.py b/backend/app/tests/api/routes/test_items.py index c215238a69..808e022504 100644 --- a/backend/app/tests/api/routes/test_items.py +++ b/backend/app/tests/api/routes/test_items.py @@ -162,3 +162,102 @@ def test_delete_item_not_enough_permissions( assert response.status_code == 400 content = response.json() assert content["detail"] == "Not enough permissions" +def test_search_items_by_title( + client: TestClient, superuser_token_headers: dict[str, str], db: Session +) -> None: + # Create test items using the existing function + item1 = create_random_item(db) + item2 = create_random_item(db) + + # Update the first item to have a specific title for testing + item1.title = "Alpha Item" + item1.description = "First item" + db.add(item1) + db.commit() + + # Test search by title + response = client.get( + f"{settings.API_V1_STR}/items/?search=Alpha", + headers=superuser_token_headers, + ) + assert response.status_code == 200 + content = response.json() + assert content["count"] == 1 + assert content["data"][0]["title"] == "Alpha Item" + + +def test_search_items_by_description( + client: TestClient, superuser_token_headers: dict[str, str], db: Session +) -> None: + # Create test items + item1 = create_random_item(db) + item2 = create_random_item(db) + + # Update the first item to have a specific description for testing + item1.title = "First" + item1.description = "UniqueAlphaDescription" # More unique + db.add(item1) + db.commit() + + # Test search by description + response = client.get( + f"{settings.API_V1_STR}/items/?search=UniqueAlpha", # More specific search + headers=superuser_token_headers, + ) + assert response.status_code == 200 + content = response.json() + assert content["count"] == 1 + assert content["data"][0]["description"] == "UniqueAlphaDescription" + + +def test_search_items_no_results( + client: TestClient, superuser_token_headers: dict[str, str], db: Session +) -> None: + # Create test items + item = create_random_item(db) + + # Update item to have specific content + item.title = "Alpha" + item.description = "First item" + db.add(item) + db.commit() + + # Test search with no matches + response = client.get( + f"{settings.API_V1_STR}/items/?search=NotFound", + headers=superuser_token_headers, + ) + assert response.status_code == 200 + content = response.json() + assert content["count"] == 0 + assert content["data"] == [] + + +def test_search_items_with_pagination( + client: TestClient, superuser_token_headers: dict[str, str], db: Session +) -> None: + # Create multiple test items + item1 = create_random_item(db) + item2 = create_random_item(db) + item3 = create_random_item(db) + + # Update items to have specific content for testing + item1.title = "Test Item 1" + item1.description = "First test" + item2.title = "Test Item 2" + item2.description = "Second test" + item3.title = "Other Item" + item3.description = "Not matching" + + db.add_all([item1, item2, item3]) + db.commit() + + # Test search with pagination + response = client.get( + f"{settings.API_V1_STR}/items/?search=Test&skip=0&limit=2", + headers=superuser_token_headers, + ) + assert response.status_code == 200 + content = response.json() + assert content["count"] == 2 + assert len(content["data"]) == 2 \ No newline at end of file diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/release-notes.md b/release-notes.md index cc477d3e7c..fa78877d0d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -149,7 +149,7 @@ * 👷 Do not sync labels as it overrides manually added labels. PR [#1307](https://github.com/fastapi/full-stack-fastapi-template/pull/1307) by [@tiangolo](https://github.com/tiangolo). * 👷 Use uv cache on GitHub Actions. PR [#1366](https://github.com/fastapi/full-stack-fastapi-template/pull/1366) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Actions format. PR [#1363](https://github.com/fastapi/full-stack-fastapi-template/pull/1363) by [@tiangolo](https://github.com/tiangolo). -* 👷 Use `uv` for Python env to generate client. PR [#1362](https://github.com/fastapi/full-stack-fastapi-template/pull/1362) by [@tiangolo](https://github.com/tiangolo). +* 👷 Use `uv` for Python env to gener ate client. PR [#1362](https://github.com/fastapi/full-stack-fastapi-template/pull/1362) by [@tiangolo](https://github.com/tiangolo). * 👷 Run tests from Python environment (with `uv`), not from Docker container. PR [#1361](https://github.com/fastapi/full-stack-fastapi-template/pull/1361) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update `generate-client.sh` script, make it fail on errors, fix generation. PR [#1360](https://github.com/fastapi/full-stack-fastapi-template/pull/1360) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Actions workflow to lint backend apart from tests. PR [#1358](https://github.com/fastapi/full-stack-fastapi-template/pull/1358) by [@tiangolo](https://github.com/tiangolo).