From b2b647ed212c45b021fecb983768ccdfe46cf613 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Tue, 30 Jul 2024 22:45:14 -0700 Subject: [PATCH] adding back some more frontend features --- frontend/src/components/files/UploadImage.tsx | 5 +- frontend/src/components/files/UploadURDF.tsx | 2 +- .../components/listing/ListingArtifacts.tsx | 110 ++++++++++++++++++ .../components/listing/ListingChildren.tsx | 1 + .../listing/ListingDeleteButton.tsx | 5 +- .../components/listing/ListingDescription.tsx | 1 + .../src/components/listing/ListingTitle.tsx | 1 + frontend/src/gen/api.ts | 50 +++++--- frontend/src/hooks/alerts.tsx | 10 ++ frontend/src/pages/ListingDetails.tsx | 21 ++-- frontend/src/pages/Listings.tsx | 7 +- frontend/src/pages/Logout.tsx | 5 +- frontend/src/pages/MyListings.tsx | 5 +- frontend/src/pages/NewListing.tsx | 5 +- frontend/src/pages/auth/Login.tsx | 10 +- frontend/src/pages/auth/Signup.tsx | 3 +- store/app/model.py | 42 ++++--- store/app/routers/artifacts.py | 47 ++++++-- store/app/routers/listings.py | 3 +- store/utils.py | 11 +- 20 files changed, 266 insertions(+), 78 deletions(-) create mode 100644 frontend/src/components/listing/ListingArtifacts.tsx diff --git a/frontend/src/components/files/UploadImage.tsx b/frontend/src/components/files/UploadImage.tsx index e04bd3b6..36dd138e 100644 --- a/frontend/src/components/files/UploadImage.tsx +++ b/frontend/src/components/files/UploadImage.tsx @@ -16,7 +16,9 @@ interface ImageUploadProps { imageId?: string | null; } -const ImageUploadComponent: React.FC = ({ imageId }) => { +const ImageUploadComponent = (props: ImageUploadProps) => { + const { imageId } = props; + const [selectedFile, setSelectedFile] = useState(null); const [compressedFile, setCompressedFile] = useState(null); const [uploadStatus, setUploadStatus] = useState(null); @@ -205,7 +207,6 @@ const ImageUploadComponent: React.FC = ({ imageId }) => { crop={crop} aspect={1} onChange={(c) => { - console.log(c); setCrop(c); }} onComplete={handleCropComplete} diff --git a/frontend/src/components/files/UploadURDF.tsx b/frontend/src/components/files/UploadURDF.tsx index c1dfec5c..fc14c191 100644 --- a/frontend/src/components/files/UploadURDF.tsx +++ b/frontend/src/components/files/UploadURDF.tsx @@ -7,7 +7,7 @@ interface URDFUploadProps { onUploadSuccess: (url: string) => void; } -const URDFUploadComponent: React.FC = () => { +const URDFUploadComponent = ({}: URDFUploadProps) => { const [selectedFile, setSelectedFile] = useState(null); const [uploadStatus, setUploadStatus] = useState(null); const [fileError, setFileError] = useState(null); diff --git a/frontend/src/components/listing/ListingArtifacts.tsx b/frontend/src/components/listing/ListingArtifacts.tsx new file mode 100644 index 00000000..5316316a --- /dev/null +++ b/frontend/src/components/listing/ListingArtifacts.tsx @@ -0,0 +1,110 @@ +import ImageComponent from "components/files/ViewImage"; +import { components } from "gen/api"; +import { useAlertQueue } from "hooks/alerts"; +import { useAuthentication } from "hooks/auth"; +import { useEffect, useState } from "react"; +import { Carousel, Row, Spinner } from "react-bootstrap"; + +const EmptyCarouselItem = ({ loading }: { loading: boolean }) => { + // TODO: Render a better default loading state. + return ( + +
+ {loading ? :

No artifacts

} +
+
+ ); +}; + +interface Props { + listing_id: string; + edit: boolean; +} + +const ListingArtifacts = (props: Props) => { + const { listing_id } = props; + + const auth = useAuthentication(); + const { addErrorAlert } = useAlertQueue(); + + const [artifacts, setArtifacts] = useState< + components["schemas"]["ListArtifactsResponse"] | null + >(null); + + useEffect(() => { + const fetchArtifacts = async () => { + const { data, error } = await auth.client.GET("/artifacts/{listing_id}", { + params: { + path: { listing_id }, + }, + }); + + if (error) { + addErrorAlert(error); + } else { + setArtifacts(data); + } + }; + fetchArtifacts(); + }, [listing_id]); + + return ( + + 1} + > + {artifacts === null || artifacts.artifacts.length === 0 ? ( + + ) : ( + artifacts.artifacts.map((artifact, key) => ( + +
+ {artifact.artifact_type === "image" ? ( + + ) : ( +

Unhandled artifact type: {artifact.artifact_type}

+ )} +
+ + {artifact.description} + +
+ )) + )} +
+
+ ); +}; + +export default ListingArtifacts; diff --git a/frontend/src/components/listing/ListingChildren.tsx b/frontend/src/components/listing/ListingChildren.tsx index 1828f272..c57894f6 100644 --- a/frontend/src/components/listing/ListingChildren.tsx +++ b/frontend/src/components/listing/ListingChildren.tsx @@ -2,6 +2,7 @@ import { Row } from "react-bootstrap"; interface Props { child_ids: string[]; + edit: boolean; } const ListingChildren = (props: Props) => { diff --git a/frontend/src/components/listing/ListingDeleteButton.tsx b/frontend/src/components/listing/ListingDeleteButton.tsx index 4ce6af53..8ced632f 100644 --- a/frontend/src/components/listing/ListingDeleteButton.tsx +++ b/frontend/src/components/listing/ListingDeleteButton.tsx @@ -1,5 +1,4 @@ import TCButton from "components/files/TCButton"; -import { humanReadableError } from "constants/backend"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useState } from "react"; @@ -14,7 +13,7 @@ const ListingDeleteButton = (props: Props) => { const { listing_id } = props; const [deleting, setDeleting] = useState(false); - const { addAlert } = useAlertQueue(); + const { addAlert, addErrorAlert } = useAlertQueue(); const auth = useAuthentication(); const navigate = useNavigate(); @@ -31,7 +30,7 @@ const ListingDeleteButton = (props: Props) => { ); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); setDeleting(false); } else { addAlert("Listing deleted successfully", "success"); diff --git a/frontend/src/components/listing/ListingDescription.tsx b/frontend/src/components/listing/ListingDescription.tsx index 4823d14a..8a7acb96 100644 --- a/frontend/src/components/listing/ListingDescription.tsx +++ b/frontend/src/components/listing/ListingDescription.tsx @@ -2,6 +2,7 @@ import { Row } from "react-bootstrap"; interface Props { description: string | null; + edit: boolean; } const ListingDescription = (props: Props) => { diff --git a/frontend/src/components/listing/ListingTitle.tsx b/frontend/src/components/listing/ListingTitle.tsx index 9cfdc60b..27a4d487 100644 --- a/frontend/src/components/listing/ListingTitle.tsx +++ b/frontend/src/components/listing/ListingTitle.tsx @@ -2,6 +2,7 @@ import { Row } from "react-bootstrap"; interface Props { title: string; + edit: boolean; } const ListingTitle = (props: Props) => { diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index 4bb37e8c..21a83db4 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -277,15 +277,15 @@ export interface paths { patch?: never; trace?: never; }; - "/artifacts/image/{image_id}/{size}": { + "/artifacts/url/{artifact_type}/{artifact_id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Images Url */ - get: operations["images_url_artifacts_image__image_id___size__get"]; + /** Artifact Url */ + get: operations["artifact_url_artifacts_url__artifact_type___artifact_id__get"]; put?: never; post?: never; delete?: never; @@ -294,15 +294,15 @@ export interface paths { patch?: never; trace?: never; }; - "/artifacts/{artifact_type}/{artifact_id}": { + "/artifacts/{listing_id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Urdf Url */ - get: operations["urdf_url_artifacts__artifact_type___artifact_id__get"]; + /** List Artifacts */ + get: operations["list_artifacts_artifacts__listing_id__get"]; put?: never; post?: never; delete?: never; @@ -392,6 +392,27 @@ export interface components { /** Detail */ detail?: components["schemas"]["ValidationError"][]; }; + /** ListArtifactsItem */ + ListArtifactsItem: { + /** Artifact Id */ + artifact_id: string; + /** + * Artifact Type + * @enum {string} + */ + artifact_type: "image" | "urdf" | "mjcf"; + /** Description */ + description: string | null; + /** Timestamp */ + timestamp: number; + /** Url */ + url: string; + }; + /** ListArtifactsResponse */ + ListArtifactsResponse: { + /** Artifacts */ + artifacts: components["schemas"]["ListArtifactsItem"][]; + }; /** ListListingsResponse */ ListListingsResponse: { /** Listings */ @@ -922,13 +943,15 @@ export interface operations { }; }; }; - images_url_artifacts_image__image_id___size__get: { + artifact_url_artifacts_url__artifact_type___artifact_id__get: { parameters: { - query?: never; + query?: { + size?: "small" | "large"; + }; header?: never; path: { - image_id: string; - size: "small" | "large"; + artifact_type: "image" | "urdf" | "mjcf"; + artifact_id: string; }; cookie?: never; }; @@ -954,13 +977,12 @@ export interface operations { }; }; }; - urdf_url_artifacts__artifact_type___artifact_id__get: { + list_artifacts_artifacts__listing_id__get: { parameters: { query?: never; header?: never; path: { - artifact_type: "urdf" | "mjcf"; - artifact_id: string; + listing_id: string; }; cookie?: never; }; @@ -972,7 +994,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["ListArtifactsResponse"]; }; }; /** @description Validation Error */ diff --git a/frontend/src/hooks/alerts.tsx b/frontend/src/hooks/alerts.tsx index 75deeb3c..dda42a4a 100644 --- a/frontend/src/hooks/alerts.tsx +++ b/frontend/src/hooks/alerts.tsx @@ -1,3 +1,4 @@ +import { humanReadableError } from "constants/backend"; import { createContext, ReactNode, @@ -29,6 +30,7 @@ interface AlertQueueContextProps { alerts: Map; removeAlert: (alertId: string) => void; addAlert: (alert: string | ReactNode, kind: AlertType) => void; + addErrorAlert: (alert: any) => void; } const AlertQueueContext = createContext( @@ -69,6 +71,13 @@ export const AlertQueueProvider = (props: AlertQueueProviderProps) => { [generateAlertId], ); + const addErrorAlert = useCallback( + (alert: any | undefined) => { + addAlert(humanReadableError(alert), "error"); + }, + [addAlert], + ); + const removeAlert = useCallback((alertId: string) => { setAlerts((prev) => { const newAlerts = new Map(prev); @@ -83,6 +92,7 @@ export const AlertQueueProvider = (props: AlertQueueProviderProps) => { alerts, removeAlert, addAlert, + addErrorAlert, }} > {children} diff --git a/frontend/src/pages/ListingDetails.tsx b/frontend/src/pages/ListingDetails.tsx index 630f827d..2804dbd4 100644 --- a/frontend/src/pages/ListingDetails.tsx +++ b/frontend/src/pages/ListingDetails.tsx @@ -1,8 +1,8 @@ +import ListingArtifacts from "components/listing/ListingArtifacts"; import ListingChildren from "components/listing/ListingChildren"; import ListingDeleteButton from "components/listing/ListingDeleteButton"; import ListingDescription from "components/listing/ListingDescription"; import ListingTitle from "components/listing/ListingTitle"; -import { humanReadableError } from "constants/backend"; import { paths } from "gen/api"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; @@ -21,16 +21,23 @@ const RenderListing = (props: RenderListingProps) => { const { listing } = props; return ( - - - + + + + {listing.owner_is_user && } ); }; const ListingDetails = () => { - const { addAlert } = useAlertQueue(); + const { addErrorAlert } = useAlertQueue(); const auth = useAuthentication(); const { id } = useParams(); const [listing, setListing] = useState(null); @@ -50,12 +57,12 @@ const ListingDetails = () => { }, }); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); } else { setListing(data); } } catch (err) { - addAlert(humanReadableError(err), "error"); + addErrorAlert(err); } }; fetchListing(); diff --git a/frontend/src/pages/Listings.tsx b/frontend/src/pages/Listings.tsx index f6bf31a7..b4c347fb 100644 --- a/frontend/src/pages/Listings.tsx +++ b/frontend/src/pages/Listings.tsx @@ -1,5 +1,4 @@ import { SearchInput } from "components/ui/Search/SearchInput"; -import { humanReadableError } from "constants/backend"; import { paths } from "gen/api"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; @@ -25,7 +24,7 @@ const Listings = () => { const [idMap, setIdMap] = useState>(new Map()); const [searchQuery, setSearchQuery] = useState(""); const [visibleSearchBarInput, setVisibleSearchBarInput] = useState(""); - const { addAlert } = useAlertQueue(); + const { addErrorAlert } = useAlertQueue(); const { page } = useParams(); const pageNumber = parseInt(page || "1", 10); @@ -61,7 +60,7 @@ const Listings = () => { }); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); return; } @@ -82,7 +81,7 @@ const Listings = () => { }); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); return; } diff --git a/frontend/src/pages/Logout.tsx b/frontend/src/pages/Logout.tsx index 840f3d81..5d448a36 100644 --- a/frontend/src/pages/Logout.tsx +++ b/frontend/src/pages/Logout.tsx @@ -1,4 +1,3 @@ -import { humanReadableError } from "constants/backend"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useEffect } from "react"; @@ -6,13 +5,13 @@ import { Spinner } from "react-bootstrap"; const Logout = () => { const auth = useAuthentication(); - const { addAlert } = useAlertQueue(); + const { addErrorAlert } = useAlertQueue(); useEffect(() => { (async () => { const { error } = await auth.client.DELETE("/users/logout"); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); } else { auth.logout(); } diff --git a/frontend/src/pages/MyListings.tsx b/frontend/src/pages/MyListings.tsx index 18e0832d..5e77940d 100644 --- a/frontend/src/pages/MyListings.tsx +++ b/frontend/src/pages/MyListings.tsx @@ -1,4 +1,3 @@ -import { humanReadableError } from "constants/backend"; import { paths } from "gen/api"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; @@ -20,7 +19,7 @@ type ListingsType = const MyListings = () => { const auth = useAuthentication(); const [partsData, setListings] = useState(null); - const { addAlert } = useAlertQueue(); + const { addErrorAlert } = useAlertQueue(); const { page } = useParams(); const [moreListings, setMoreListings] = useState(false); @@ -47,7 +46,7 @@ const MyListings = () => { }); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); } else { setListings(data.listings); setMoreListings(data.has_next); diff --git a/frontend/src/pages/NewListing.tsx b/frontend/src/pages/NewListing.tsx index a9ee081c..706c97c3 100644 --- a/frontend/src/pages/NewListing.tsx +++ b/frontend/src/pages/NewListing.tsx @@ -1,5 +1,4 @@ import TCButton from "components/files/TCButton"; -import { humanReadableError } from "constants/backend"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { Dispatch, FormEvent, SetStateAction, useState } from "react"; @@ -66,7 +65,7 @@ const NewListing = () => { const [name, setName] = useState(""); const [description, setDescription] = useState(""); - const { addAlert } = useAlertQueue(); + const { addAlert, addErrorAlert } = useAlertQueue(); const navigate = useNavigate(); // On submit, add the listing to the database and navigate to the @@ -83,7 +82,7 @@ const NewListing = () => { }); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); } else { addAlert("Listing added successfully", "success"); navigate(`/listing/${data.listing_id}`); diff --git a/frontend/src/pages/auth/Login.tsx b/frontend/src/pages/auth/Login.tsx index 4575d126..d72c05e2 100644 --- a/frontend/src/pages/auth/Login.tsx +++ b/frontend/src/pages/auth/Login.tsx @@ -3,7 +3,6 @@ import { Button } from "components/ui/Button/Button"; import CardWrapper from "components/ui/Card/CardWrapper"; import ErrorMessage from "components/ui/ErrorMessage"; import { Input } from "components/ui/Input/Input"; -import { humanReadableError } from "constants/backend"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useEffect, useState } from "react"; @@ -22,24 +21,23 @@ const Login = () => { }); const auth = useAuthentication(); - const { addAlert } = useAlertQueue(); + const { addErrorAlert } = useAlertQueue(); const [useSpinner, setUseSpinner] = useState(false); const [showPassword, setShowPassword] = useState(false); const onSubmit: SubmitHandler = async (data: LoginType) => { - // add an api endpoint to send the credentials details to backend + // TODO: Add an api endpoint to send the credentials details to backend. console.log(data); }; const handleGithubSubmit = async ( event: React.MouseEvent, ) => { - console.log("event : ", event); event.preventDefault(); const { data, error } = await auth.client.GET("/users/github/login"); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); } else { window.open(data, "_self"); } @@ -59,7 +57,7 @@ const Login = () => { }); if (error) { - addAlert(humanReadableError(error), "error"); + addErrorAlert(error); setUseSpinner(false); } else { auth.login(data.api_key); diff --git a/frontend/src/pages/auth/Signup.tsx b/frontend/src/pages/auth/Signup.tsx index 7226a34c..c0414c60 100644 --- a/frontend/src/pages/auth/Signup.tsx +++ b/frontend/src/pages/auth/Signup.tsx @@ -22,9 +22,10 @@ const Signup = () => { }); const onSubmit: SubmitHandler = async (data: LoginType) => { - // add an api endpoint to send the credentials details to backend and email verification + // TODO: Add an api endpoint to send the credentials details to backend and email verification. console.log(data); }; + return (
Self: - return cls(id=str(new_uuid()), email=email, permissions=None) + return cls(id=new_uuid(), email=email, permissions=None) class OAuthKey(RobolistBaseModel): @@ -53,7 +53,7 @@ class OAuthKey(RobolistBaseModel): @classmethod def create(cls, user_token: str, user_id: str) -> Self: - return cls(id=str(new_uuid()), user_id=user_id, user_token=user_token) + return cls(id=new_uuid(), user_id=user_id, user_token=user_token) APIKeySource = Literal["user", "oauth"] @@ -84,7 +84,7 @@ def create( if permissions == "full": permissions = {"read", "write", "admin"} ttl_timestamp = int((datetime.utcnow() + timedelta(days=90)).timestamp()) - return cls(id=str(new_uuid()), user_id=user_id, source=source, permissions=permissions, ttl=ttl_timestamp) + return cls(id=new_uuid(), user_id=user_id, source=source, permissions=permissions, ttl=ttl_timestamp) ArtifactSize = Literal["small", "large"] @@ -108,15 +108,7 @@ def create( } -@overload -def get_artifact_name(id: str, artifact_type: Literal["image"], size: ArtifactSize) -> str: ... - - -@overload -def get_artifact_name(id: str, artifact_type: Literal["urdf", "mjcf"]) -> str: ... - - -def get_artifact_name(id: str, artifact_type: ArtifactType, size: ArtifactSize | None = None) -> str: +def get_artifact_name(id: str, artifact_type: ArtifactType, size: ArtifactSize = "large") -> str: match artifact_type: case "image": if size is None: @@ -131,6 +123,10 @@ def get_artifact_name(id: str, artifact_type: ArtifactType, size: ArtifactSize | raise ValueError(f"Unknown artifact type: {artifact_type}") +def get_artifact_url(id: str, artifact_type: ArtifactType, size: ArtifactSize = "large") -> str: + return f"{settings.site.artifact_base_url}/{get_artifact_name(id, artifact_type, size)}" + + def get_content_type(artifact_type: ArtifactType) -> str: return DOWNLOAD_CONTENT_TYPE[artifact_type] @@ -163,7 +159,7 @@ def create( description: str | None = None, ) -> Self: return cls( - id=str(new_uuid()), + id=new_uuid(), user_id=user_id, listing_id=listing_id, name=name, @@ -186,6 +182,22 @@ class Listing(RobolistBaseModel): child_ids: list[str] description: str | None + @classmethod + def create( + cls, + user_id: str, + name: str, + child_ids: list[str], + description: str | None = None, + ) -> Self: + return cls( + id=new_uuid(), + user_id=user_id, + name=name, + child_ids=child_ids, + description=description, + ) + class ListingTag(RobolistBaseModel): """Marks a listing as having a given tag. @@ -201,7 +213,7 @@ class ListingTag(RobolistBaseModel): @classmethod def create(cls, listing_id: str, tag: str) -> Self: return cls( - id=str(new_uuid()), + id=new_uuid(), listing_id=listing_id, name=tag, ) diff --git a/store/app/routers/artifacts.py b/store/app/routers/artifacts.py index 5a010761..747260b7 100644 --- a/store/app/routers/artifacts.py +++ b/store/app/routers/artifacts.py @@ -1,17 +1,15 @@ """Defines the router endpoints for handling listing artifacts.""" import logging -from typing import Annotated, Literal +from typing import Annotated from fastapi import APIRouter, Depends, Form, HTTPException, UploadFile, status from fastapi.responses import RedirectResponse from pydantic.main import BaseModel -from store.app.crud.artifacts import get_artifact_name from store.app.db import Crud -from store.app.model import UPLOAD_CONTENT_TYPE_OPTIONS, ArtifactSize, ArtifactType, User +from store.app.model import UPLOAD_CONTENT_TYPE_OPTIONS, ArtifactSize, ArtifactType, User, get_artifact_url from store.app.routers.users import get_session_user_with_write_permission -from store.settings import settings artifacts_router = APIRouter() @@ -22,17 +20,42 @@ class UploadImageResponse(BaseModel): image_id: str -@artifacts_router.get("/image/{image_id}/{size}") -async def images_url(image_id: str, size: ArtifactSize) -> RedirectResponse: +@artifacts_router.get("/url/{artifact_type}/{artifact_id}") +async def artifact_url( + artifact_type: ArtifactType, + artifact_id: str, + size: ArtifactSize = "large", +) -> RedirectResponse: # TODO: Use CloudFront API to return a signed CloudFront URL. - image_url = f"{settings.site.artifact_base_url}/{get_artifact_name(image_id, 'image', size)}" - return RedirectResponse(url=image_url) + return RedirectResponse(url=get_artifact_url(artifact_id, artifact_type, size)) -@artifacts_router.get("/{artifact_type}/{artifact_id}") -async def urdf_url(artifact_type: Literal["urdf", "mjcf"], artifact_id: str) -> RedirectResponse: - artifact_url = f"{settings.site.artifact_base_url}/{get_artifact_name(artifact_id, artifact_type)}" - return RedirectResponse(url=artifact_url) +class ListArtifactsItem(BaseModel): + artifact_id: str + artifact_type: ArtifactType + description: str | None + timestamp: int + url: str + + +class ListArtifactsResponse(BaseModel): + artifacts: list[ListArtifactsItem] + + +@artifacts_router.get("/{listing_id}", response_model=ListArtifactsResponse) +async def list_artifacts(listing_id: str, crud: Annotated[Crud, Depends(Crud.get)]) -> ListArtifactsResponse: + return ListArtifactsResponse( + artifacts=[ + ListArtifactsItem( + artifact_id=artifact.id, + artifact_type=artifact.artifact_type, + description=artifact.description, + timestamp=artifact.timestamp, + url=get_artifact_url(artifact.id, artifact.artifact_type), + ) + for artifact in await crud.get_listing_artifacts(listing_id) + ], + ) class UploadArtifactRequest(BaseModel): diff --git a/store/app/routers/listings.py b/store/app/routers/listings.py index ae3e681f..cd7dfe4d 100644 --- a/store/app/routers/listings.py +++ b/store/app/routers/listings.py @@ -81,8 +81,7 @@ async def add_listing( user: Annotated[User, Depends(get_session_user_with_write_permission)], crud: Annotated[Crud, Depends(Crud.get)], ) -> NewListingResponse: - listing = Listing( - id=str(new_uuid()), + listing = Listing.create( name=new_listing.name, description=new_listing.description, user_id=user.id, diff --git a/store/utils.py b/store/utils.py index b46487d5..47c63f2d 100644 --- a/store/utils.py +++ b/store/utils.py @@ -2,6 +2,7 @@ import datetime import functools +import hashlib import uuid from collections import OrderedDict from typing import Awaitable, Callable, Generic, Hashable, ParamSpec, TypeVar, overload @@ -140,5 +141,11 @@ def server_time() -> datetime.datetime: return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) -def new_uuid() -> uuid.UUID: - return uuid.uuid4() +def new_uuid() -> str: + """Generate a new UUID. + + Returns: + A new UUID, as a string, with the first 16 characters of the + SHA-256 hash of a UUID4 value. + """ + return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:16]