diff --git a/frontend/src/components/listings/ListingGridCard.tsx b/frontend/src/components/listings/ListingGridCard.tsx index b475943d..3329e770 100644 --- a/frontend/src/components/listings/ListingGridCard.tsx +++ b/frontend/src/components/listings/ListingGridCard.tsx @@ -32,7 +32,11 @@ const ListingGridCard = (props: Props) => { onMouseLeave={() => setHovering(false)} onClick={() => navigate(`/item/${listingId}`)} > - + {listing?.image_url ? ( + {listing.name} + ) : ( + + )}
@@ -43,7 +47,7 @@ const ListingGridCard = (props: Props) => { )} - + {listing ? ( listing?.description && ( diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index 5b548b80..5d5ad739 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -478,6 +478,8 @@ export interface components { description: string | null; /** Child Ids */ child_ids: string[]; + /** Image Url */ + image_url: string | null; }; /** NewListingRequest */ NewListingRequest: { diff --git a/store/app/crud/artifacts.py b/store/app/crud/artifacts.py index 8934a5ea..66721e01 100644 --- a/store/app/crud/artifacts.py +++ b/store/app/crud/artifacts.py @@ -49,6 +49,10 @@ def _crop_image(self, image: Image.Image, size: tuple[int, int]) -> io.BytesIO: lower = upper + new_height image_resized = image.crop((left, upper, right, lower)) + # Resize the image to the desired size. + image_resized = image_resized.resize(size, resample=Image.Resampling.BICUBIC) + + # Save the image to a byte stream. image_resized.save(image_bytes, format="PNG", optimize=True, quality=settings.image.quality) image_bytes.seek(0) return image_bytes @@ -167,6 +171,9 @@ async def remove_artifact(self, artifact: Artifact, user_id: str) -> None: async def get_listing_artifacts(self, listing_id: str) -> list[Artifact]: return await self._get_items_from_secondary_index("listing_id", listing_id, Artifact) + async def get_listings_artifacts(self, listing_ids: list[str]) -> list[list[Artifact]]: + return await self._get_items_from_secondary_index_batch("listing_id", listing_ids, Artifact) + async def edit_artifact( self, artifact_id: str, diff --git a/store/app/crud/base.py b/store/app/crud/base.py index e5bdce62..403b4755 100644 --- a/store/app/crud/base.py +++ b/store/app/crud/base.py @@ -7,7 +7,7 @@ from typing import Any, AsyncContextManager, BinaryIO, Callable, Literal, Self, TypeVar, overload import aioboto3 -from boto3.dynamodb.conditions import Key +from boto3.dynamodb.conditions import Attr, Key from botocore.exceptions import ClientError from types_aiobotocore_dynamodb.service_resource import DynamoDBServiceResource from types_aiobotocore_s3.service_resource import S3ServiceResource @@ -226,8 +226,14 @@ async def _get_item_batch( chunk = item_ids[i : i + chunk_size] keys = [{"id": item_id} for item_id in chunk] response = await self.db.batch_get_item(RequestItems={TABLE_NAME: {"Keys": keys}}) + + # Maps the items to their IDs to return them in the correct order. + item_ids_to_items: dict[str, T] = {} for item in response["Responses"][TABLE_NAME]: - items.append(self._validate_item(item, item_class)) + item_impl = self._validate_item(item, item_class) + item_ids_to_items[item_impl.id] = item_impl + items += [item_ids_to_items[item_id] for item_id in chunk] + return items async def _get_items_from_secondary_index( @@ -245,6 +251,40 @@ async def _get_items_from_secondary_index( items = item_dict["Items"] return [self._validate_item(item, item_class) for item in items] + async def _get_items_from_secondary_index_batch( + self, + secondary_index_name: str, + secondary_index_values: list[str], + item_class: type[T], + chunk_size: int = DEFAULT_CHUNK_SIZE, + ) -> list[list[T]]: + items: list[list[T]] = [] + table = await self.db.Table(TABLE_NAME) + + for i in range(0, len(secondary_index_values), chunk_size): + chunk = secondary_index_values[i : i + chunk_size] + response = await table.scan( + IndexName=self.get_gsi_index_name(secondary_index_name), + FilterExpression=(Attr(secondary_index_name).is_in(chunk) & Attr("type").eq(item_class.__name__)), + ) + + # Maps the items to their IDs. + chunk_items = [self._validate_item(item, item_class) for item in response["Items"]] + chunk_ids_to_items: dict[str, list[T]] = {} + for item in chunk_items: + item_id = getattr(item, secondary_index_name) + if item_id in chunk_ids_to_items: + chunk_ids_to_items[item_id].append(item) + else: + chunk_ids_to_items[item_id] = [item] + + print(chunk_ids_to_items) + + # Adds the items to the list. + items += [chunk_ids_to_items.get(id, []) for id in chunk] + + return items + @overload async def _get_unique_item_from_secondary_index( self, diff --git a/store/app/routers/listings.py b/store/app/routers/listings.py index 3ee970eb..37b009b8 100644 --- a/store/app/routers/listings.py +++ b/store/app/routers/listings.py @@ -8,7 +8,7 @@ from pydantic import BaseModel from store.app.db import Crud -from store.app.model import Listing, User +from store.app.model import Listing, User, get_artifact_url from store.app.routers.users import ( get_session_user_with_read_permission, get_session_user_with_write_permission, @@ -41,6 +41,7 @@ class ListingInfoResponse(BaseModel): name: str description: str | None child_ids: list[str] + image_url: str | None class GetBatchListingsResponse(BaseModel): @@ -52,7 +53,10 @@ async def get_batch_listing_info( crud: Annotated[Crud, Depends(Crud.get)], ids: list[str] = Query(description="List of part ids"), ) -> GetBatchListingsResponse: - listings = await crud._get_item_batch(ids, Listing) + listings, artifacts = await asyncio.gather( + crud._get_item_batch(ids, Listing), + crud.get_listings_artifacts(ids), + ) return GetBatchListingsResponse( listings=[ ListingInfoResponse( @@ -60,8 +64,16 @@ async def get_batch_listing_info( name=listing.name, description=listing.description, child_ids=listing.child_ids, + image_url=next( + ( + get_artifact_url(artifact.id, "image", "small") + for artifact in artifacts + if artifact.artifact_type == "image" + ), + None, + ), ) - for listing in listings + for listing, artifacts in zip(listings, artifacts) ] )