From 0477fa80733caf6db0b36356c72273f4b7844420 Mon Sep 17 00:00:00 2001 From: yaswanthrajeev Date: Sun, 13 Jul 2025 22:35:38 +0530 Subject: [PATCH 1/3] Add search functionality to items endpoint with tests --- backend/app/api/routes/items.py | 26 ++++-- backend/app/tests/api/routes/test_items.py | 99 ++++++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py index 177dc1e476..6f1b588ae7 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,45 @@ @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) ) - items = session.exec(statement).all() + + + if search: + search_filter = or_( + Item.title.contains(search, autoescape=True), + Item.description.contains(search, autoescape=True), + ) + 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 From 710c3ad1861bcca087a72a5928a03a3c3b8bc5a5 Mon Sep 17 00:00:00 2001 From: yaswanthrajeev Date: Mon, 14 Jul 2025 16:10:19 +0530 Subject: [PATCH 2/3] search function added --- backend/app/api/routes/items.py | 4 ++-- openapi.json | 0 release-notes.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 openapi.json diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py index 6f1b588ae7..85a2c9f4c1 100644 --- a/backend/app/api/routes/items.py +++ b/backend/app/api/routes/items.py @@ -44,8 +44,8 @@ def read_items( if search: search_filter = or_( - Item.title.contains(search, autoescape=True), - Item.description.contains(search, autoescape=True), + Item.title.contains(search, autoescape=True), # type: ignore + Item.description.contains(search, autoescape=True), # type: ignore ) query = query.where(search_filter) count_statement = count_statement.where(search_filter) 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). From 66cce41ba845d02d89051dc2e4b9a75069d3905f Mon Sep 17 00:00:00 2001 From: yaswanthrajeev Date: Tue, 15 Jul 2025 01:36:15 +0530 Subject: [PATCH 3/3] search function added --- backend/app/api/routes/items.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py index 85a2c9f4c1..7dfac1a6ea 100644 --- a/backend/app/api/routes/items.py +++ b/backend/app/api/routes/items.py @@ -25,7 +25,6 @@ def read_items( if current_user.is_superuser: query = select(Item) count_statement = select(func.count()).select_from(Item) - statement = select(Item).offset(skip).limit(limit) else: query = select(Item).where(Item.owner_id == current_user.id) count_statement = ( @@ -33,19 +32,11 @@ def read_items( .select_from(Item) .where(Item.owner_id == current_user.id) ) - - 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 + Item.title.contains(search, autoescape=True), # type: ignore + Item.description.contains(search, autoescape=True), # type: ignore ) query = query.where(search_filter) count_statement = count_statement.where(search_filter)