From bbd48d002bafb6496800b1214839a57a6cc01a92 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:06:12 +0100 Subject: [PATCH 1/5] feat(ui-utils): add `validatePath` function in validation --- webapp/src/utils/validation/array.ts | 1 - webapp/src/utils/validation/number.ts | 3 +- webapp/src/utils/validation/string.ts | 104 +++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/webapp/src/utils/validation/array.ts b/webapp/src/utils/validation/array.ts index d0edd43a15..98c7b50251 100644 --- a/webapp/src/utils/validation/array.ts +++ b/webapp/src/utils/validation/array.ts @@ -29,7 +29,6 @@ interface ArrayValidationOptions { * validateArray([1, 2, 3], { allowDuplicate: false }); // true * validateArray([1, 1, 2, 3], { allowDuplicate: false }); // Error message * - * * @example With currying. * const fn = validateArray({ allowDuplicate: false }); * fn([1, 2, 3]); // true diff --git a/webapp/src/utils/validation/number.ts b/webapp/src/utils/validation/number.ts index fd96c1502e..f975ef29ba 100644 --- a/webapp/src/utils/validation/number.ts +++ b/webapp/src/utils/validation/number.ts @@ -28,11 +28,10 @@ interface NumberValidationOptions { * validateNumber(5, { min: 0, max: 10 }); // true * validateNumber(9, { min: 10, max: 20 }); // Error message * - * * @example With currying. * const fn = validateNumber({ min: 0, max: 10 }); * fn(5); // true - * fn(11); // Error message + * fn(9); // Error message * * @param value - The number to validate. * @param [options] - Configuration options for validation. diff --git a/webapp/src/utils/validation/string.ts b/webapp/src/utils/validation/string.ts index bd9bdef1e8..bf47d6775c 100644 --- a/webapp/src/utils/validation/string.ts +++ b/webapp/src/utils/validation/string.ts @@ -33,6 +33,15 @@ interface StringValidationOptions { * Validates the input string against a variety of checks including length restrictions, * character validations, and uniqueness against provided arrays of existing and excluded values. * + * @example + * validateString("foo", { allowSpaces: false }); // true + * validateNumber("foo bar", { allowSpaces: false }); // Error message + * + * @example With currying. + * const fn = validateString({ allowSpaces: false }); + * fn("foo"); // true + * fn("foo bar"); // Error message + * * @param value - The string to validate. Leading and trailing spaces will be trimmed. * @param options - Configuration options for validation. (Optional) * @param [options.existingValues=[]] - An array of strings to check against for duplicates. Comparison is case-insensitive by default. @@ -49,7 +58,22 @@ interface StringValidationOptions { export function validateString( value: string, options?: StringValidationOptions, -): ValidationReturn { +): ValidationReturn; + +export function validateString( + options?: StringValidationOptions, +): (value: string) => ValidationReturn; + +export function validateString( + valueOrOpts?: string | StringValidationOptions, + options: StringValidationOptions = {}, +): ValidationReturn | ((value: string) => ValidationReturn) { + if (typeof valueOrOpts !== "string") { + return (v: string) => validateString(v, valueOrOpts); + } + + const value = valueOrOpts; + const { existingValues = [], excludedValues = [], @@ -60,7 +84,7 @@ export function validateString( editedValue = "", minLength = 0, maxLength = 255, - } = options || {}; + } = options; const trimmedValue = value.trim(); @@ -183,3 +207,79 @@ function generatePattern( allowSpecialChars && specialChars ? escapeSpecialChars(specialChars) : ""; return basePattern + spacePattern + specialCharsPattern + "]*$"; } + +interface PathValidationOptions { + allowToStartWithSlash?: boolean; + allowToEndWithSlash?: boolean; + allowEmpty?: boolean; +} + +/** + * Validates a path against specified criteria. + * + * @example + * validatePath("foo/bar", { allowToEndWithSlash: false }); // true + * validatePath("foo/bar/", { allowToEndWithSlash: false }); // Error message + * + * @example With currying. + * const fn = validateString({ allowToEndWithSlash: false }); + * fn("foo/bar"); // true + * fn("foo/bar/"); // Error message + * + * @param path - The string to validate. + * @param options - Configuration options for validation. (Optional) + * @param [options.allowToStartWithSlash=true] - Indicates if the path is allowed to start with a '/'. + * @param [options.allowToEndWithSlash=true] - Indicates if the path is allowed to end with a '/'. + * @param [options.allowEmpty=false] - Indicates if an empty path is allowed. + * @returns True if validation is successful, or a localized error message if it fails. + */ +export function validatePath( + path: string, + options?: PathValidationOptions, +): ValidationReturn; + +export function validatePath( + options?: PathValidationOptions, +): (value: string) => ValidationReturn; + +export function validatePath( + pathOrOpts?: string | PathValidationOptions, + options: PathValidationOptions = {}, +): ValidationReturn | ((value: string) => ValidationReturn) { + if (typeof pathOrOpts !== "string") { + return (v: string) => validatePath(v, pathOrOpts); + } + + const path = pathOrOpts; + + const { + allowToStartWithSlash = true, + allowToEndWithSlash = true, + allowEmpty = false, + } = options; + + if (!path) { + return allowEmpty ? true : t("form.field.required"); + } + + if (!allowToStartWithSlash && path.startsWith("/")) { + return t("form.field.path.startWithSlashNotAllowed"); + } + + if (!allowToEndWithSlash && path.endsWith("/")) { + return t("form.field.path.endWithSlashNotAllowed"); + } + + if ( + path + .replace(/^\//, "") // Remove first "/" if present + .replace(/\/$/, "") // Remove last "/" if present + .split("/") + .map((v) => v.trim()) + .includes("") + ) { + return t("form.field.path.invalid"); + } + + return true; +} From e7496836c5088ae7b3c35dce14e928d6655ad4a6 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:10:56 +0100 Subject: [PATCH 2/5] fix(ui-studies): small bottom part not visible with scroll --- .../App/Studies/StudiesList/index.tsx | 7 ++----- webapp/src/components/App/Studies/StudyTree.tsx | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index a6c412e625..6d90785471 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -39,10 +39,7 @@ import { FixedSizeGrid, GridOnScrollProps } from "react-window"; import { v4 as uuidv4 } from "uuid"; import { AxiosError } from "axios"; import { StudyMetadata } from "../../../../common/types"; -import { - STUDIES_HEIGHT_HEADER, - STUDIES_LIST_HEADER_HEIGHT, -} from "../../../../theme"; +import { STUDIES_LIST_HEADER_HEIGHT } from "../../../../theme"; import { setStudyScrollPosition, StudiesSortConf, @@ -184,7 +181,7 @@ function StudiesList(props: StudiesListProps) { return ( { - return children.map((elm) => { - const id = parentId ? `${parentId}/${elm.name}` : elm.name; + return children.map((child) => { + const id = parentId ? `${parentId}/${child.name}` : child.name; return ( handleTreeItemClick(id)} > - {buildTree(elm.children, id)} + {buildTree(child.children, id)} ); }); @@ -60,7 +60,14 @@ function StudyTree() { {buildTree([studiesTree])} From 68c49fac25305f082062671e4860bad3b54892c5 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:11:56 +0100 Subject: [PATCH 3/5] refactor(ui-studies): function to build tree --- webapp/src/components/App/Studies/utils.ts | 75 +++++++++++----------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/webapp/src/components/App/Studies/utils.ts b/webapp/src/components/App/Studies/utils.ts index dc2d1c5396..3f2ff61564 100644 --- a/webapp/src/components/App/Studies/utils.ts +++ b/webapp/src/components/App/Studies/utils.ts @@ -20,46 +20,45 @@ export interface StudyTreeNode { children: StudyTreeNode[]; } -const nodeProcess = ( - tree: StudyTreeNode, - path: string[], - folderPath: string, -): void => { - const { children } = tree; - if (path.length === 1) { - return; - } - const element = path.pop() || ""; - const index = children.findIndex( - (elm: StudyTreeNode) => elm.name === element, - ); - const newFolderPath = `${folderPath}/${element}`; - if (index < 0) { - children.push({ name: element, children: [], path: newFolderPath }); - nodeProcess( - children[children.length - 1] as StudyTreeNode, - path, - newFolderPath, - ); - } else { - nodeProcess(children[index] as StudyTreeNode, path, newFolderPath); - } -}; - -export const buildStudyTree = (studies: StudyMetadata[]): StudyTreeNode => { +/** + * Builds a tree structure from a list of study metadata. + * + * @param studies - Array of study metadata objects. + * @returns A tree structure representing the studies. + */ +export function buildStudyTree(studies: StudyMetadata[]) { const tree: StudyTreeNode = { name: "root", children: [], path: "" }; - let path: string[] = []; + for (const study of studies) { - if (study.folder !== undefined && study.folder !== null) { - path = [ - study.workspace, - ...(study.folder as string).split("/").filter((elm) => elm !== ""), - ]; - } else { - path = [study.workspace]; + const path = + typeof study.folder === "string" + ? [study.workspace, ...study.folder.split("/").filter(Boolean)] + : [study.workspace]; + + let current = tree; + + for (let i = 0; i < path.length; i++) { + // Skip the last folder, as it represents the study itself + if (i === path.length - 1) { + break; + } + + const folderName = path[i]; + let child = current.children.find((child) => child.name === folderName); + + if (!child) { + child = { + name: folderName, + children: [], + path: current.path ? `${current.path}/${folderName}` : folderName, + }; + + current.children.push(child); + } + + current = child; } - path.reverse(); - nodeProcess(tree, path, ""); } + return tree; -}; +} From 373f8942f0f16df93daee3e9335a6abe01de8556 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:12:53 +0100 Subject: [PATCH 4/5] feat(ui-studies): update validation in MoveStudyDialog ANT-2390 --- webapp/public/locales/en/main.json | 8 +- webapp/public/locales/fr/main.json | 8 +- .../App/Studies/MoveStudyDialog.tsx | 96 +++++++++++-------- webapp/src/services/api/study.ts | 13 ++- 4 files changed, 71 insertions(+), 54 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index df948d3920..98abe54461 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -78,6 +78,7 @@ "global.status": "Status", "global.semicolon": "Semicolon", "global.language": "Language", + "global.path": "Path", "global.time.hourly": "Hourly", "global.time.daily": "Daily", "global.time.weekly": "Weekly", @@ -154,6 +155,9 @@ "form.field.requireUppercase": "Must contain at least one uppercase letter.", "form.field.requireDigit": "Must contain at least one digit.", "form.field.requireSpecialChars": "Must contain at least one special character.", + "form.field.path.startWithSlashNotAllowed": "Path must not start with a '/'", + "form.field.path.endWithSlashNotAllowed": "Path must not end with a '/'", + "form.field.path.invalid": "Invalid path", "matrix.graphSelector": "Columns", "matrix.message.importHint": "Click or drag and drop a matrix here", "matrix.importNewMatrix": "Import a new matrix", @@ -620,7 +624,6 @@ "studies.error.loadStudy": "Failed to load study", "studies.error.runStudy": "Failed to run study", "studies.error.scanFolder": "Failed to start folder scan", - "studies.error.moveStudy": "Failed to move study {{study}}", "studies.error.saveData": "Failed to save data", "studies.error.copyStudy": "Failed to copy study", "studies.error.import": "Failed to import Study ({{uploadFile}})", @@ -631,11 +634,10 @@ "studies.error.createStudy": "Failed to create Study {{studyname}}", "studies.success.saveData": "Data saved with success", "studies.success.scanFolder": "Folder scan started", - "studies.success.moveStudy": "Study {{study}} was successfully moved to {{folder}}", + "studies.success.moveStudy": "Study '{{study}}' was successfully moved to '{{path}}'", "studies.success.createStudy": "Study {{studyname}} created successfully", "studies.studylaunched": "{{studyname}} launched!", "studies.copySuffix": "Copy", - "studies.folder": "Folder", "studies.filters.strictfolder": "Show only direct folder children", "studies.scanFolder": "Scan folder", "studies.moveStudy": "Move", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 3dacacb6e0..8e64c877be 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -78,6 +78,7 @@ "global.status": "Statut", "global.semicolon": "Point-virgule", "global.language": "Langue", + "global.path": "Chemin", "global.time.hourly": "Horaire", "global.time.daily": "Journalier", "global.time.weekly": "Hebdomadaire", @@ -154,6 +155,9 @@ "form.field.requireUppercase": "Doit contenir au moins une lettre majuscule.", "form.field.requireDigit": "Doit contenir au moins un chiffre.", "form.field.requireSpecialChars": "Doit contenir au moins un caractère spécial.", + "form.field.path.startWithSlashNotAllowed": "Le chemin ne doit pas commencer par un '/'", + "form.field.path.endWithSlashNotAllowed": "Le chemin ne doit pas finir par un '/'", + "form.field.path.invalid": "Chemin invalide", "matrix.graphSelector": "Colonnes", "matrix.message.importHint": "Cliquer ou glisser une matrice ici", "matrix.importNewMatrix": "Import d'une nouvelle matrice", @@ -620,7 +624,6 @@ "studies.error.loadStudy": "Échec du chargement de l'étude", "studies.error.runStudy": "Échec du lancement de l'étude", "studies.error.scanFolder": "Échec du lancement du scan", - "studies.error.moveStudy": "Échec du déplacement de l'étude {{study}}", "studies.error.saveData": "Erreur lors de la sauvegarde des données", "studies.error.copyStudy": "Erreur lors de la copie de l'étude", "studies.error.import": "L'import de l'étude a échoué ({{uploadFile}})", @@ -631,11 +634,10 @@ "studies.error.createStudy": "Erreur lors de la création de l'étude {{studyname}}", "studies.success.saveData": "Donnée sauvegardée avec succès", "studies.success.scanFolder": "L'analyse du dossier a commencé", - "studies.success.moveStudy": "L'étude {{study}} a été déplacée avec succès vers {{folder}}", + "studies.success.moveStudy": "L'étude \"{{study}}\" a été déplacée avec succès vers \"{{path}}\"", "studies.success.createStudy": "L'étude {{studyname}} a été crée avec succès", "studies.studylaunched": "{{studyname}} lancé(s) !", "studies.copySuffix": "Copie", - "studies.folder": "Dossier", "studies.filters.strictfolder": "Afficher uniquement les descendants directs", "studies.scanFolder": "Scanner le dossier", "studies.moveStudy": "Déplacer", diff --git a/webapp/src/components/App/Studies/MoveStudyDialog.tsx b/webapp/src/components/App/Studies/MoveStudyDialog.tsx index e3698438a5..ff9687f8af 100644 --- a/webapp/src/components/App/Studies/MoveStudyDialog.tsx +++ b/webapp/src/components/App/Studies/MoveStudyDialog.tsx @@ -13,17 +13,40 @@ */ import { DialogProps } from "@mui/material"; -import TextField from "@mui/material/TextField"; import { useSnackbar } from "notistack"; -import * as R from "ramda"; import { useTranslation } from "react-i18next"; -import { usePromise } from "react-use"; import { StudyMetadata } from "../../../common/types"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; import { moveStudy } from "../../../services/api/study"; -import { isStringEmpty } from "../../../services/utils"; import FormDialog from "../../common/dialogs/FormDialog"; import { SubmitHandlerPlus } from "../../common/Form/types"; +import StringFE from "@/components/common/fieldEditors/StringFE"; +import * as R from "ramda"; +import { validatePath } from "@/utils/validation/string"; + +function formalizePath( + path: string | undefined, + studyId?: StudyMetadata["id"], +) { + const trimmedPath = path?.trim(); + + if (!trimmedPath) { + return ""; + } + + const pathArray = trimmedPath.split("/").filter(Boolean); + + if (studyId) { + const lastFolder = R.last(pathArray); + + // The API automatically add the study ID to a not empty path when moving a study. + // So we need to remove it from the display path. + if (lastFolder === studyId) { + return pathArray.slice(0, -1).join("/"); + } + } + + return pathArray.join("/"); +} interface Props extends DialogProps { study: StudyMetadata; @@ -33,36 +56,35 @@ interface Props extends DialogProps { function MoveStudyDialog(props: Props) { const { study, open, onClose } = props; const [t] = useTranslation(); - const mounted = usePromise(); const { enqueueSnackbar } = useSnackbar(); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); + const defaultValues = { - folder: R.join("/", R.dropLast(1, R.split("/", study.folder || ""))), + path: formalizePath(study.folder, study.id), }; //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// - const handleSubmit = async ( + const handleSubmit = (data: SubmitHandlerPlus) => { + const path = formalizePath(data.values.path); + return moveStudy(study.id, path); + }; + + const handleSubmitSuccessful = ( data: SubmitHandlerPlus, ) => { - const { folder } = data.values; - try { - await mounted(moveStudy(study.id, folder)); - enqueueSnackbar( - t("studies.success.moveStudy", { study: study.name, folder }), - { - variant: "success", - }, - ); - onClose(); - } catch (e) { - enqueueErrorSnackbar( - t("studies.error.moveStudy", { study: study.name }), - e as Error, - ); - } + onClose(); + + enqueueSnackbar( + t("studies.success.moveStudy", { + study: study.name, + path: data.values.path || "/", // Empty path move the study to the root + }), + { + variant: "success", + }, + ); }; //////////////////////////////////////////////////////////////// @@ -74,27 +96,19 @@ function MoveStudyDialog(props: Props) { open={open} config={{ defaultValues }} onSubmit={handleSubmit} + onSubmitSuccessful={handleSubmitSuccessful} onCancel={onClose} > - {(formObj) => ( - ( + { - return !isStringEmpty(value); - }, - })} /> )} diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 8750b177f4..53d4a67925 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -13,7 +13,7 @@ */ import { AxiosRequestConfig } from "axios"; -import { isBoolean, trimCharsStart } from "ramda-adjunct"; +import * as RA from "ramda-adjunct"; import client from "./client"; import { FileStudyTreeConfigDTO, @@ -135,7 +135,7 @@ export const editStudy = async ( depth = 1, ): Promise => { let formattedData: unknown = data; - if (isBoolean(data)) { + if (RA.isBoolean(data)) { formattedData = JSON.stringify(data); } const res = await client.post( @@ -163,11 +163,10 @@ export const copyStudy = async ( return res.data; }; -export const moveStudy = async (sid: string, folder: string): Promise => { - const folderWithId = trimCharsStart("/", `${folder.trim()}/${sid}`); - await client.put( - `/v1/studies/${sid}/move?folder_dest=${encodeURIComponent(folderWithId)}`, - ); +export const moveStudy = async (studyId: string, folder: string) => { + await client.put(`/v1/studies/${studyId}/move`, null, { + params: { folder_dest: folder }, + }); }; export const archiveStudy = async (sid: string): Promise => { From 6d454f4aac26d3c4b09230333307d0d81e94802a Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 5 Dec 2024 11:40:04 +0100 Subject: [PATCH 5/5] feat(move): adapt back-end code and add tests --- antarest/study/service.py | 6 ++- .../variantstudy/variant_study_service.py | 2 +- .../studies_blueprint/test_move.py | 51 +++++++++++++++++++ tests/integration/test_integration.py | 3 +- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 tests/integration/studies_blueprint/test_move.py diff --git a/antarest/study/service.py b/antarest/study/service.py index 7e1198c6b7..71d4bc3881 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -1073,11 +1073,15 @@ def copy_task(notifier: ITaskNotifier) -> TaskResult: return task_or_study_id - def move_study(self, study_id: str, new_folder: str, params: RequestParameters) -> None: + def move_study(self, study_id: str, folder_dest: str, params: RequestParameters) -> None: study = self.get_study(study_id) assert_permission(params.user, study, StudyPermissionType.WRITE) if not is_managed(study): raise NotAManagedStudyException(study_id) + if folder_dest: + new_folder = folder_dest.rstrip("/") + f"/{study.id}" + else: + new_folder = None study.folder = new_folder self.repository.save(study, update_modification_date=False) self.event_bus.push( diff --git a/antarest/study/storage/variantstudy/variant_study_service.py b/antarest/study/storage/variantstudy/variant_study_service.py index a57b7f1b2f..37493be684 100644 --- a/antarest/study/storage/variantstudy/variant_study_service.py +++ b/antarest/study/storage/variantstudy/variant_study_service.py @@ -610,7 +610,7 @@ def create_variant_study(self, uuid: str, name: str, params: RequestParameters) created_at=datetime.utcnow(), updated_at=datetime.utcnow(), version=study.version, - folder=(re.sub(f"/?{study.id}", "", study.folder) if study.folder is not None else None), + folder=(re.sub(study.id, new_id, study.folder) if study.folder is not None else None), groups=study.groups, # Create inherit_group boolean owner_id=params.user.impersonator if params.user else None, snapshot=None, diff --git a/tests/integration/studies_blueprint/test_move.py b/tests/integration/studies_blueprint/test_move.py new file mode 100644 index 0000000000..83defc9aad --- /dev/null +++ b/tests/integration/studies_blueprint/test_move.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from starlette.testclient import TestClient + + +class TestMove: + def test_move_endpoint(self, client: TestClient, internal_study_id: str, user_access_token: str) -> None: + client.headers = {"Authorization": f"Bearer {user_access_token}"} + + res = client.post("/v1/studies?name=study_test") + assert res.status_code == 201 + study_id = res.json() + + # asserts move with a given folder adds the /study_id at the end of the path + res = client.put(f"/v1/studies/{study_id}/move", params={"folder_dest": "folder1"}) + res.raise_for_status() + res = client.get(f"/v1/studies/{study_id}") + assert res.json()["folder"] == f"folder1/{study_id}" + + # asserts move to a folder with //// removes the unwanted `/` + res = client.put(f"/v1/studies/{study_id}/move", params={"folder_dest": "folder2///////"}) + res.raise_for_status() + res = client.get(f"/v1/studies/{study_id}") + assert res.json()["folder"] == f"folder2/{study_id}" + + # asserts the created variant has the same parent folder + res = client.post(f"/v1/studies/{study_id}/variants?name=Variant1") + variant_id = res.json() + res = client.get(f"/v1/studies/{variant_id}") + assert res.json()["folder"] == f"folder2/{variant_id}" + + # asserts move doesn't work on un-managed studies + res = client.put(f"/v1/studies/{internal_study_id}/move", params={"folder_dest": "folder1"}) + assert res.status_code == 422 + assert res.json()["exception"] == "NotAManagedStudyException" + + # asserts users can put back a study at the root folder + res = client.put(f"/v1/studies/{study_id}/move", params={"folder_dest": ""}) + res.raise_for_status() + res = client.get(f"/v1/studies/{study_id}") + assert res.json()["folder"] is None diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 27cb8cf78a..0f691d733f 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -207,7 +207,8 @@ def test_main(client: TestClient, admin_access_token: str) -> None: headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, ) assert len(res.json()) == 3 - assert filter(lambda s: s["id"] == copied.json(), res.json().values()).__next__()["folder"] == "foo/bar" + moved_study = filter(lambda s: s["id"] == copied.json(), res.json().values()).__next__() + assert moved_study["folder"] == f"foo/bar/{moved_study['id']}" # Study delete client.delete(