diff --git a/backend/app/api/routes/projects.py b/backend/app/api/routes/projects.py index bae6baf0..55d159b6 100644 --- a/backend/app/api/routes/projects.py +++ b/backend/app/api/routes/projects.py @@ -240,7 +240,7 @@ def create_project( logger.warning(f"Failed to create: {resp.json()}") try: message = resp.json()["errors"][0]["message"].capitalize() - except: + except Exception: message = "Failed to create GitHub repo" raise HTTPException(resp.status_code, message) resp_json = resp.json() @@ -1227,7 +1227,31 @@ def get_project_figure( current_user: CurrentUser, session: SessionDep, ) -> Figure: - raise HTTPException(501) + project = app.projects.get_project( + session=session, + owner_name=owner_name, + project_name=project_name, + current_user=current_user, + min_access_level="read", + ) + ck_info = get_ck_info( + project=project, user=current_user, session=session, ttl=120 + ) + figures = ck_info.get("figures", []) + # Get the figure content and base64 encode it + for fig in figures: + if fig.get("path") == figure_path: + item = get_project_contents( + owner_name=owner_name, + project_name=project_name, + session=session, + current_user=current_user, + path=fig["path"], + ) + fig["content"] = item.content + fig["url"] = item.url + return Figure.model_validate(fig) + raise HTTPException(404, "Figure not found") @router.post("/projects/{owner_name}/{project_name}/figures") @@ -2632,3 +2656,76 @@ def get_project_app( if project_app is None: return return ProjectApp.model_validate(project_app) + + +class ProjectShowcaseFigureInput(BaseModel): + figure: str + + +class ProjectShowcaseFigure(BaseModel): + figure: Figure + + +class ProjectShowcaseText(BaseModel): + text: str + + +class ProjectShowcaseInput(BaseModel): + elements: list[ProjectShowcaseFigureInput | ProjectShowcaseText] + + +class ProjectShowcase(BaseModel): + elements: list[ProjectShowcaseFigure | ProjectShowcaseText] + + +@router.get("/projects/{owner_name}/{project_name}/showcase") +def get_project_showcase( + owner_name: str, + project_name: str, + current_user: CurrentUser, + session: SessionDep, +) -> ProjectShowcase | None: + project = app.projects.get_project( + owner_name=owner_name, + project_name=project_name, + session=session, + current_user=current_user, + min_access_level="read", + ) + incorrectly_defined = ProjectShowcase( + elements=[ + ProjectShowcaseText(text="Showcase is not correctly defined.") + ] + ) + ck_info = get_ck_info( + project=project, user=current_user, session=session, ttl=120 + ) + showcase = ck_info.get("showcase") + if showcase is None: + return + try: + inputs = ProjectShowcaseInput.model_validate(dict(elements=showcase)) + except Exception: + return incorrectly_defined + # Iterate over showcase elements, fetching the contents to return + elements_out = [] + for element_in in inputs.elements: + if isinstance(element_in, ProjectShowcaseFigureInput): + try: + element_out = ProjectShowcaseFigure( + figure=get_project_figure( + owner_name=owner_name, + project_name=project_name, + session=session, + current_user=current_user, + figure_path=element_in.figure, + ) + ) + except Exception: + element_out = ProjectShowcaseText( + text=f"Figure at path '{element_in.figure}' not found" + ) + else: + element_out = element_in + elements_out.append(element_out) + return ProjectShowcase.model_validate(dict(elements=elements_out)) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4f65562c..9d4bc8e6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -58,27 +58,9 @@ strict = true exclude = ["venv", ".venv", "alembic"] [tool.ruff] -target-version = "py310" +target-version = "py312" exclude = ["alembic"] -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade - "ARG001", # unused arguments in functions -] -ignore = [ - "E501", # line too long, handled by black - "B008", # do not perform function calls in argument defaults - "W191", # indentation contains tabs - "B904", # Allow raising exceptions without from e, for HTTPException -] - [tool.ruff.lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true diff --git a/frontend/src/client/models.ts b/frontend/src/client/models.ts index 9ae5e9b3..224356d6 100644 --- a/frontend/src/client/models.ts +++ b/frontend/src/client/models.ts @@ -434,6 +434,18 @@ export type ProjectPublic = { current_user_access?: "read" | "write" | "admin" | "owner" | null } +export type ProjectShowcase = { + elements: Array +} + +export type ProjectShowcaseFigure = { + figure: Figure +} + +export type ProjectShowcaseText = { + text: string +} + export type ProjectsPublic = { data: Array count: number diff --git a/frontend/src/client/schemas.ts b/frontend/src/client/schemas.ts index 285d8fb0..79936f5f 100644 --- a/frontend/src/client/schemas.ts +++ b/frontend/src/client/schemas.ts @@ -2169,6 +2169,44 @@ export const $ProjectPublic = { }, } as const +export const $ProjectShowcase = { + properties: { + elements: { + type: "array", + contains: { + type: "any-of", + contains: [ + { + type: "ProjectShowcaseFigure", + }, + { + type: "ProjectShowcaseText", + }, + ], + }, + isRequired: true, + }, + }, +} as const + +export const $ProjectShowcaseFigure = { + properties: { + figure: { + type: "Figure", + isRequired: true, + }, + }, +} as const + +export const $ProjectShowcaseText = { + properties: { + text: { + type: "string", + isRequired: true, + }, + }, +} as const + export const $ProjectsPublic = { properties: { data: { diff --git a/frontend/src/client/services.ts b/frontend/src/client/services.ts index 4fbb1b32..11a02ad6 100644 --- a/frontend/src/client/services.ts +++ b/frontend/src/client/services.ts @@ -54,6 +54,7 @@ import type { ProjectCreate, ProjectPatch, ProjectPublic, + ProjectShowcase, ProjectsPublic, Publication, Question, @@ -379,6 +380,10 @@ export type ProjectsData = { ownerName: string projectName: string } + GetProjectShowcase: { + ownerName: string + projectName: string + } } export type OrgsData = { @@ -2129,6 +2134,28 @@ export class ProjectsService { }, }) } + + /** + * Get Project Showcase + * @returns unknown Successful Response + * @throws ApiError + */ + public static getProjectShowcase( + data: ProjectsData["GetProjectShowcase"], + ): CancelablePromise { + const { ownerName, projectName } = data + return __request(OpenAPI, { + method: "GET", + url: "/projects/{owner_name}/{project_name}/showcase", + path: { + owner_name: ownerName, + project_name: projectName, + }, + errors: { + 422: `Validation Error`, + }, + }) + } } export class OrgsService { diff --git a/frontend/src/components/Common/ActionsMenu.tsx b/frontend/src/components/Common/ActionsMenu.tsx index 2773297c..95e20d7c 100644 --- a/frontend/src/components/Common/ActionsMenu.tsx +++ b/frontend/src/components/Common/ActionsMenu.tsx @@ -21,7 +21,7 @@ interface ActionsMenuProps { } const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { - const editUserModal = useDisclosure() + const editEntityModal = useDisclosure() const deleteModal = useDisclosure() return ( @@ -35,7 +35,7 @@ const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { /> } > Edit {type.toLowerCase()} @@ -51,14 +51,14 @@ const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { {type === "User" ? ( ) : ( )} Not set + if (figure.path.endsWith(".pdf")) { + figView = ( + + + + ) + } else if ( + figure.path.endsWith(".png") || + figure.path.endsWith(".jpg") || + figure.path.endsWith(".jpeg") + ) { + figView = ( + + {figure.title} + + ) + } else if (figure.path.endsWith(".json")) { + const figObject = JSON.parse(atob(String(figure.content))) + const layout = figObject.layout + figView = ( + + + + ) + } else if (figure.path.endsWith(".html")) { + // Embed HTML figure in an iframe + const { data, isPending } = useQuery({ + queryFn: () => axios.get(String(figure.url)), + queryKey: [ + "projects", + userName, + projectName, + "figure-content", + figure.path, + ], + enabled: Boolean(!figure.content && figure.url), + }) + let figContent = figure.content + if (!figure.content && figure.url) { + figContent = data?.data + } else { + figContent = "No content found" + } + figView = ( + + {figContent ? ( +