From 3f54f55cd8627d17c35d07ca4d81808015d41c8b Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 29 Jul 2024 14:25:22 -0500 Subject: [PATCH 01/14] Initial backend implementation of doi --- backend/app/config.py | 6 +++ backend/app/models/datasets.py | 1 + backend/app/routers/datasets.py | 40 ++++++++++++++++ backend/app/routers/doi.py | 48 +++++++++++++++++++ .../src/openapi/v2/models/DatasetFreezeOut.ts | 1 + frontend/src/openapi/v2/models/DatasetOut.ts | 1 + .../openapi/v2/services/DatasetsService.ts | 23 +++++++++ 7 files changed, 120 insertions(+) create mode 100644 backend/app/routers/doi.py diff --git a/backend/app/config.py b/backend/app/config.py index 2f3041853..888b44359 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -88,5 +88,11 @@ class Settings(BaseSettings): # defautl listener heartbeat time interval in seconds 5 minutes listener_heartbeat_interval = 5 * 60 + # DOI datacite details + DATACITE_TEST_URL = "https://api.test.datacite.org/dois" + DATACITE_URL = "https://api.datacite.org/dois" + DATACITE_USERNAME = "admin" + DATACITE_PASSWORD = "" + settings = Settings() diff --git a/backend/app/models/datasets.py b/backend/app/models/datasets.py index bfecfb1af..5a3bc743a 100644 --- a/backend/app/models/datasets.py +++ b/backend/app/models/datasets.py @@ -44,6 +44,7 @@ class DatasetBaseCommon(DatasetBase): origin_id: Optional[PydanticObjectId] = None standard_license: bool = True license_id: Optional[str] = None + doi: Optional[str] = None class DatasetPatch(BaseModel): diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index d5f4b3de3..ca92b3a20 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -74,6 +74,8 @@ from rocrate.model.person import Person from rocrate.rocrate import ROCrate +from backend.app.routers.doi import DataCiteClient + router = APIRouter() security = HTTPBearer() @@ -482,6 +484,44 @@ async def delete_dataset( raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") +@router.post("/{dataset_id}/doi", response_model=str) +async def mint_doi( + dataset_id: str, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization(RoleType.OWNER)), +): + dataset = await DatasetDB.get(PydanticObjectId(dataset_id)) + if dataset is None: + return f"Dataset {dataset_id} not found" + dataset_db = dataset.dict() + metadata = { + "data": { + "type": "dois", + "attributes": { + "prefix": "10.1234", + "doi": "10.1234/your-doi-suffix", + "url": "https://your.url", + "titles": [{"title": dataset_db["name"]}], + "creators": [ + {"name": dataset_db["creator"]["first_name"]["last_name"]} + ], + "publisher": "DataCite e.V.", + "publicationYear": datetime.now().year, + }, + } + } + dataCiteClient = DataCiteClient() + response = dataCiteClient.create_doi(metadata) + dataset_db["doi"] = response.json()["data"]["id"] + dataset_db["modified"] = datetime.datetime.utcnow() + await dataset_db.save() + + # Update entry to the dataset index + await index_dataset(es, DatasetOut(dataset_db), update=True) + + @router.post("/{dataset_id}/freeze", response_model=DatasetFreezeOut) async def freeze_dataset( dataset_id: str, diff --git a/backend/app/routers/doi.py b/backend/app/routers/doi.py new file mode 100644 index 000000000..cda91e62d --- /dev/null +++ b/backend/app/routers/doi.py @@ -0,0 +1,48 @@ +from app.config import settings +from fastapi import requests +from requests.auth import HTTPBasicAuth + + +class DataCiteClient: + def __init__(self, test_mode=True): + self.auth = HTTPBasicAuth(settings.USERNAME, settings.PASSWORD) + self.headers = {"Content-Type": "application/vnd.api+json"} + self.base_url = ( + "https://api.test.datacite.org/" + if test_mode + else "https://api.datacite.org/" + ) + + def create_doi(self, metadata): + url = f"{self.base_url}dois" + response = requests.post( + url, auth=self.auth, headers=self.headers, json=metadata + ) + return response.json() + + def get_all_dois(self): + url = f"{self.base_url}dois" + response = requests.get(url, auth=self.auth, headers=self.headers) + return response.json() + + def get_doi(self, doi): + url = f"{self.base_url}dois/{doi}" + response = requests.get(url, auth=self.auth, headers=self.headers) + return response.json() + + def update_doi(self, doi, metadata): + url = f"{self.base_url}dois/{doi}" + response = requests.put( + url, auth=self.auth, headers=self.headers, json=metadata + ) + return response.json() + + def delete_doi(self, doi): + url = f"{self.base_url}dois/{doi}" + response = requests.delete(url, auth=self.auth, headers=self.headers) + return response.status_code == 204 + + def get_doi_activity_status(self, doi): + url = f"{self.base_url}events?doi={doi}" + response = requests.get(url, auth=self.auth, headers=self.headers) + return response.json() diff --git a/frontend/src/openapi/v2/models/DatasetFreezeOut.ts b/frontend/src/openapi/v2/models/DatasetFreezeOut.ts index f98189e9b..fbdd5f998 100644 --- a/frontend/src/openapi/v2/models/DatasetFreezeOut.ts +++ b/frontend/src/openapi/v2/models/DatasetFreezeOut.ts @@ -30,6 +30,7 @@ export type DatasetFreezeOut = { origin_id?: string; standard_license?: boolean; license_id?: string; + doi?: string; id?: string; frozen?: boolean; frozen_version_num: number; diff --git a/frontend/src/openapi/v2/models/DatasetOut.ts b/frontend/src/openapi/v2/models/DatasetOut.ts index cfd47b50c..0107b72fb 100644 --- a/frontend/src/openapi/v2/models/DatasetOut.ts +++ b/frontend/src/openapi/v2/models/DatasetOut.ts @@ -30,6 +30,7 @@ export type DatasetOut = { origin_id?: string; standard_license?: boolean; license_id?: string; + doi?: string; id?: string; frozen?: boolean; frozen_version_num?: number; diff --git a/frontend/src/openapi/v2/services/DatasetsService.ts b/frontend/src/openapi/v2/services/DatasetsService.ts index 11db1018b..200312fe6 100644 --- a/frontend/src/openapi/v2/services/DatasetsService.ts +++ b/frontend/src/openapi/v2/services/DatasetsService.ts @@ -240,6 +240,29 @@ export class DatasetsService { }); } + /** + * Mint Doi + * @param datasetId + * @param enableAdmin + * @returns string Successful Response + * @throws ApiError + */ + public static mintDoiApiV2DatasetsDatasetIdDoiPost( + datasetId: string, + enableAdmin: boolean = false, + ): CancelablePromise { + return __request({ + method: 'POST', + path: `/api/v2/datasets/${datasetId}/doi`, + query: { + 'enable_admin': enableAdmin, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** * Get Freeze Datasets * @param datasetId From 349b236310a9beace625bf6c9e34e33ed1ddb009 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 29 Jul 2024 14:30:57 -0500 Subject: [PATCH 02/14] fix --- backend/app/routers/datasets.py | 438 ++++++++++++++++---------------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index ca92b3a20..72bfe26e2 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -74,7 +74,7 @@ from rocrate.model.person import Person from rocrate.rocrate import ROCrate -from backend.app.routers.doi import DataCiteClient +from app.routers.doi import DataCiteClient router = APIRouter() security = HTTPBearer() @@ -148,12 +148,12 @@ def nested_update(target_dict, update_dict): async def _create_folder_structure( - dataset_id: str, - contents: dict, - folder_path: str, - folder_lookup: dict, - user: UserOut, - parent_folder_id: Optional[str] = None, + dataset_id: str, + contents: dict, + folder_path: str, + folder_lookup: dict, + user: UserOut, + parent_folder_id: Optional[str] = None, ): """Recursively create folders encountered in folder_path until the target folder is created. Arguments: @@ -189,10 +189,10 @@ async def _create_folder_structure( @router.post("", response_model=DatasetOut) async def save_dataset( - dataset_in: DatasetIn, - license_id: str, - user=Depends(get_current_user), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + dataset_in: DatasetIn, + license_id: str, + user=Depends(get_current_user), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): standard_license = False standard_license_ids = [license.id for license in standard_licenses] @@ -221,13 +221,13 @@ async def save_dataset( @router.get("", response_model=Paged) async def get_datasets( - user_id=Depends(get_user), - skip: int = 0, - limit: int = 10, - mine: bool = False, - admin=Depends(get_admin), - enable_admin: bool = False, - admin_mode: bool = Depends(get_admin_mode), + user_id=Depends(get_user), + skip: int = 0, + limit: int = 10, + mine: bool = False, + admin=Depends(get_admin), + enable_admin: bool = False, + admin_mode: bool = Depends(get_admin_mode), ): query = [DatasetDBViewList.frozen == False] # noqa: E712 @@ -282,18 +282,18 @@ async def get_datasets( @router.get("/{dataset_id}", response_model=DatasetOut) async def get_dataset( - dataset_id: str, - authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), - public: bool = Depends(CheckStatus("PUBLIC")), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), + public: bool = Depends(CheckStatus("PUBLIC")), + allow: bool = Depends(Authorization("viewer")), ): if authenticated or public or allow: if ( - dataset := await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), + dataset := await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), + ) ) - ) ) is not None: return dataset.dict() raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") @@ -303,24 +303,24 @@ async def get_dataset( @router.get("/{dataset_id}/files", response_model=Paged) async def get_dataset_files( - dataset_id: str, - folder_id: Optional[str] = None, - authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), - public: bool = Depends(CheckStatus("PUBLIC")), - user_id=Depends(get_user), - skip: int = 0, - limit: int = 10, - admin=Depends(get_admin), - enable_admin: bool = False, - admin_mode: bool = Depends(get_admin_mode), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + folder_id: Optional[str] = None, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), + public: bool = Depends(CheckStatus("PUBLIC")), + user_id=Depends(get_user), + skip: int = 0, + limit: int = 10, + admin=Depends(get_admin), + enable_admin: bool = False, + admin_mode: bool = Depends(get_admin_mode), + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), + ) ) - ) ) is not None: if authenticated or public or (admin and admin_mode): query = [ @@ -359,11 +359,11 @@ async def get_dataset_files( @router.put("/{dataset_id}", response_model=DatasetOut) async def edit_dataset( - dataset_id: str, - dataset_info: DatasetBase, - user=Depends(get_current_user), - es=Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + dataset_info: DatasetBase, + user=Depends(get_current_user), + es=Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: # TODO: Refactor this with permissions checks etc. @@ -376,7 +376,7 @@ async def edit_dataset( # Update folders index since its using dataset downloads and status to index async for folder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id) + FolderDB.dataset_id == PydanticObjectId(dataset_id) ): await index_folder(es, FolderOut(**folder.dict()), update=True) @@ -386,11 +386,11 @@ async def edit_dataset( @router.patch("/{dataset_id}", response_model=DatasetOut) async def patch_dataset( - dataset_id: str, - dataset_info: DatasetPatch, - user=Depends(get_current_user), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + dataset_info: DatasetPatch, + user=Depends(get_current_user), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: # TODO: Update method not working properly @@ -410,7 +410,7 @@ async def patch_dataset( files_views = await FileDBViewList.find(*query).to_list() for file_view in files_views: if ( - file := await FileDB.get(PydanticObjectId(file_view.id)) + file := await FileDB.get(PydanticObjectId(file_view.id)) ) is not None: file.status = dataset_info.status await file.save() @@ -421,7 +421,7 @@ async def patch_dataset( # Update folders index since its using dataset downloads and status to index async for folder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id) + FolderDB.dataset_id == PydanticObjectId(dataset_id) ): await index_folder(es, FolderOut(**folder.dict()), update=True) @@ -430,10 +430,10 @@ async def patch_dataset( @router.delete("/{dataset_id}") async def delete_dataset( - dataset_id: str, - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: # delete from elasticsearch @@ -441,7 +441,7 @@ async def delete_dataset( # find associate frozen datasets and delete them iteratively async for frozen_dataset in DatasetFreezeDB.find( - DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id) + DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id) ): await _delete_frozen_dataset(frozen_dataset, fs, hard_delete=True) @@ -464,7 +464,7 @@ async def delete_dataset( # delete files and its associate resources async for file in FileDB.find( - FileDB.dataset_id == PydanticObjectId(dataset_id) + FileDB.dataset_id == PydanticObjectId(dataset_id) ): await remove_file_entry(file.id, fs, es) @@ -486,11 +486,11 @@ async def delete_dataset( @router.post("/{dataset_id}/doi", response_model=str) async def mint_doi( - dataset_id: str, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization(RoleType.OWNER)), + dataset_id: str, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization(RoleType.OWNER)), ): dataset = await DatasetDB.get(PydanticObjectId(dataset_id)) if dataset is None: @@ -524,11 +524,11 @@ async def mint_doi( @router.post("/{dataset_id}/freeze", response_model=DatasetFreezeOut) async def freeze_dataset( - dataset_id: str, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization(RoleType.OWNER)), + dataset_id: str, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization(RoleType.OWNER)), ): # Retrieve the dataset by ID if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: @@ -551,7 +551,7 @@ async def freeze_dataset( frozen_dataset_data["frozen_version_num"] = 1 else: frozen_dataset_data["frozen_version_num"] = ( - latest_frozen_dataset.frozen_version_num + 1 + latest_frozen_dataset.frozen_version_num + 1 ) # start freezing associate information @@ -592,13 +592,13 @@ async def freeze_dataset( @router.get("/{dataset_id}/freeze", response_model=Paged) async def get_freeze_datasets( - dataset_id: str, - skip: int = 0, - limit: int = 10, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + skip: int = 0, + limit: int = 10, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): frozen_datasets_and_count = ( await DatasetFreezeDB.find( @@ -627,11 +627,11 @@ async def get_freeze_datasets( @router.get("/{dataset_id}/freeze/latest_version_num", response_model=int) async def get_freeze_dataset_lastest_version_num( - dataset_id: str, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): freeze_dataset_latest_version_num = -999 latest_frozen_dataset = ( @@ -651,19 +651,19 @@ async def get_freeze_dataset_lastest_version_num( "/{dataset_id}/freeze/{frozen_version_num}", response_model=DatasetFreezeOut ) async def get_freeze_dataset_version( - dataset_id: str, - frozen_version_num: int, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + frozen_version_num: int, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): # Retrieve the dataset by ID if ( - frozen_dataset := await DatasetFreezeDB.find_one( - DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), - DatasetFreezeDB.frozen_version_num == frozen_version_num, - ) + frozen_dataset := await DatasetFreezeDB.find_one( + DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), + DatasetFreezeDB.frozen_version_num == frozen_version_num, + ) ) is not None: if frozen_dataset.deleted is True: raise HTTPException( @@ -683,19 +683,19 @@ async def get_freeze_dataset_version( "/{dataset_id}/freeze/{frozen_version_num}", response_model=DatasetFreezeOut ) async def delete_freeze_dataset_version( - dataset_id: str, - frozen_version_num: int, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + frozen_version_num: int, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): # Retrieve the frozen dataset by ID if ( - frozen_dataset := await DatasetFreezeDB.find_one( - DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), - DatasetFreezeDB.frozen_version_num == frozen_version_num, - ) + frozen_dataset := await DatasetFreezeDB.find_one( + DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), + DatasetFreezeDB.frozen_version_num == frozen_version_num, + ) ) is not None: return await _delete_frozen_dataset(frozen_dataset, fs, hard_delete=False) @@ -707,11 +707,11 @@ async def delete_freeze_dataset_version( @router.post("/{dataset_id}/folders", response_model=FolderOut) async def add_folder( - dataset_id: str, - folder_in: FolderIn, - user=Depends(get_current_user), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + folder_in: FolderIn, + user=Depends(get_current_user), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("uploader")), ): if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: parent_folder = folder_in.parent_folder @@ -731,21 +731,21 @@ async def add_folder( @router.get("/{dataset_id}/folders", response_model=Paged) async def get_dataset_folders( - dataset_id: str, - parent_folder: Optional[str] = None, - user_id=Depends(get_user), - authenticated: bool = Depends(CheckStatus("authenticated")), - public: bool = Depends(CheckStatus("PUBLIC")), - skip: int = 0, - limit: int = 10, - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + parent_folder: Optional[str] = None, + user_id=Depends(get_user), + authenticated: bool = Depends(CheckStatus("authenticated")), + public: bool = Depends(CheckStatus("PUBLIC")), + skip: int = 0, + limit: int = 10, + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), + ) ) - ) ) is not None: if authenticated or public: query = [ @@ -787,24 +787,24 @@ async def get_dataset_folders( @router.get("/{dataset_id}/folders_and_files", response_model=Paged) async def get_dataset_folders_and_files( - dataset_id: str, - folder_id: Optional[str] = None, - authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), - public: bool = Depends(CheckStatus("PUBLIC")), - user_id=Depends(get_user), - skip: int = 0, - limit: int = 10, - admin=Depends(get_admin), - enable_admin: bool = False, - admin_mode: bool = Depends(get_admin_mode), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + folder_id: Optional[str] = None, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), + public: bool = Depends(CheckStatus("PUBLIC")), + user_id=Depends(get_user), + skip: int = 0, + limit: int = 10, + admin=Depends(get_admin), + enable_admin: bool = False, + admin_mode: bool = Depends(get_admin_mode), + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), + ) ) - ) ) is not None: if authenticated or public or (admin and admin_mode): query = [ @@ -869,11 +869,11 @@ async def get_dataset_folders_and_files( @router.delete("/{dataset_id}/folders/{folder_id}") async def delete_folder( - dataset_id: str, - folder_id: str, - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + folder_id: str, + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None: @@ -884,14 +884,14 @@ async def delete_folder( # recursively delete child folder and files async def _delete_nested_folders(parent_folder_id): while ( - await FolderDB.find_one( - FolderDB.dataset_id == ObjectId(dataset_id), - FolderDB.parent_folder == ObjectId(parent_folder_id), - ) + await FolderDB.find_one( + FolderDB.dataset_id == ObjectId(dataset_id), + FolderDB.parent_folder == ObjectId(parent_folder_id), + ) ) is not None: async for subfolder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id), - FolderDB.parent_folder == PydanticObjectId(parent_folder_id), + FolderDB.dataset_id == PydanticObjectId(dataset_id), + FolderDB.parent_folder == PydanticObjectId(parent_folder_id), ): async for file in FileDB.find(FileDB.folder_id == subfolder.id): await remove_file_entry(file.id, fs, es) @@ -910,16 +910,16 @@ async def _delete_nested_folders(parent_folder_id): @router.get("/{dataset_id}/folders/{folder_id}") async def get_folder( - dataset_id: str, - folder_id: str, - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + folder_id: str, + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), + ) ) - ) ) is not None: if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None: return folder.dict() @@ -930,12 +930,12 @@ async def get_folder( @router.patch("/{dataset_id}/folders/{folder_id}", response_model=FolderOut) async def patch_folder( - dataset_id: str, - folder_id: str, - folder_info: FolderPatch, - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - user=Depends(get_current_user), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + folder_id: str, + folder_info: FolderPatch, + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + user=Depends(get_current_user), + allow: bool = Depends(Authorization("editor")), ): if await DatasetDB.get(PydanticObjectId(dataset_id)) is not None: if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None: @@ -945,8 +945,8 @@ async def patch_folder( # allow moving folder around within the hierarchy if folder_info.parent_folder is not None: if ( - await FolderDB.get(PydanticObjectId(folder_info.parent_folder)) - is not None + await FolderDB.get(PydanticObjectId(folder_info.parent_folder)) + is not None ): folder.parent_folder = folder_info.parent_folder folder.modified = datetime.datetime.utcnow() @@ -965,14 +965,14 @@ async def patch_folder( @router.post("/{dataset_id}/files", response_model=FileOut) async def save_file( - dataset_id: str, - folder_id: Optional[str] = None, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - file: UploadFile = File(...), - es=Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + folder_id: Optional[str] = None, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + file: UploadFile = File(...), + es=Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if user is None: @@ -1017,14 +1017,14 @@ async def save_file( @router.post("/{dataset_id}/filesMultiple", response_model=List[FileOut]) async def save_files( - dataset_id: str, - files: List[UploadFile], - folder_id: Optional[str] = None, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es=Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + files: List[UploadFile], + folder_id: Optional[str] = None, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es=Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: files_added = [] @@ -1044,7 +1044,7 @@ async def save_files( if folder_id is not None: if ( - folder := await FolderDB.get(PydanticObjectId(folder_id)) + folder := await FolderDB.get(PydanticObjectId(folder_id)) ) is not None: new_file.folder_id = folder.id else: @@ -1078,13 +1078,13 @@ async def save_files( @router.post("/{dataset_id}/local_files", response_model=FileOut) async def save_local_file( - localfile_in: LocalFileIn, - dataset_id: str, - folder_id: Optional[str] = None, - user=Depends(get_current_user), - es=Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + localfile_in: LocalFileIn, + dataset_id: str, + folder_id: Optional[str] = None, + user=Depends(get_current_user), + es=Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if user is None: @@ -1133,12 +1133,12 @@ async def save_local_file( @router.post("/createFromZip", response_model=DatasetOut) async def create_dataset_from_zip( - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - file: UploadFile = File(...), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - token: str = Depends(get_token), + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + file: UploadFile = File(...), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + token: str = Depends(get_token), ): if file.filename.endswith(".zip") is False: raise HTTPException(status_code=404, detail="File is not a zip file") @@ -1206,16 +1206,16 @@ async def create_dataset_from_zip( @router.get("/{dataset_id}/download") async def download_dataset( - dataset_id: str, - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + allow: bool = Depends(Authorization("viewer")), ): if ( - dataset := await DatasetDBViewList.find_one( - DatasetDBViewList.id == PydanticObjectId(dataset_id) - ) + dataset := await DatasetDBViewList.find_one( + DatasetDBViewList.id == PydanticObjectId(dataset_id) + ) ) is not None: current_temp_dir = tempfile.mkdtemp(prefix="rocratedownload") crate = ROCrate() @@ -1260,7 +1260,7 @@ async def download_dataset( file_count = 0 async for file in FileDBViewList.find( - FileDBViewList.dataset_id == ObjectId(dataset_id) + FileDBViewList.dataset_id == ObjectId(dataset_id) ): # find the bytes id # if it's working draft file_id == origin_id @@ -1376,7 +1376,7 @@ async def download_dataset( await index_dataset(es, DatasetOut(**dataset.dict()), update=True) # Update folders index since its using dataset downloads and status to index async for folder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id) + FolderDB.dataset_id == PydanticObjectId(dataset_id) ): await index_folder(es, FolderOut(**folder.dict()), update=True) @@ -1388,14 +1388,14 @@ async def download_dataset( # can handle parameeters pass in as key/values in info @router.post("/{dataset_id}/extract") async def get_dataset_extract( - dataset_id: str, - extractorName: str, - request: Request, - # parameters don't have a fixed model shape - parameters: dict = None, - user=Depends(get_current_user), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + extractorName: str, + request: Request, + # parameters don't have a fixed model shape + parameters: dict = None, + user=Depends(get_current_user), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if extractorName is None: raise HTTPException(status_code=400, detail="No extractorName specified") @@ -1415,17 +1415,17 @@ async def get_dataset_extract( @router.get("/{dataset_id}/thumbnail") async def download_dataset_thumbnail( - dataset_id: str, - fs: Minio = Depends(dependencies.get_fs), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + fs: Minio = Depends(dependencies.get_fs), + allow: bool = Depends(Authorization("viewer")), ): # If dataset exists in MongoDB, download from Minio if ( - dataset := await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), + dataset := await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), + ) ) - ) ) is not None: if dataset.thumbnail_id is not None: content = fs.get_object( @@ -1448,9 +1448,9 @@ async def download_dataset_thumbnail( @router.patch("/{dataset_id}/thumbnail/{thumbnail_id}", response_model=DatasetOut) async def add_dataset_thumbnail( - dataset_id: str, - thumbnail_id: str, - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + thumbnail_id: str, + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if (await ThumbnailDB.get(PydanticObjectId(thumbnail_id))) is not None: From 6d362e82f532ac01db2bcc867254e93e4af5152d Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 29 Jul 2024 16:46:34 -0500 Subject: [PATCH 03/14] working backend and added testcase for minting doi for dataset --- backend/app/config.py | 3 +- backend/app/routers/datasets.py | 500 +++++++++--------- backend/app/routers/doi.py | 11 +- backend/app/tests/test_datasets.py | 15 + frontend/src/app.config.ts | 2 + .../openapi/v2/services/DatasetsService.ts | 13 +- 6 files changed, 286 insertions(+), 258 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 888b44359..9cc2fa8b7 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -89,10 +89,9 @@ class Settings(BaseSettings): listener_heartbeat_interval = 5 * 60 # DOI datacite details + DOI_ENABLED = True DATACITE_TEST_URL = "https://api.test.datacite.org/dois" DATACITE_URL = "https://api.datacite.org/dois" - DATACITE_USERNAME = "admin" - DATACITE_PASSWORD = "" settings = Settings() diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 72bfe26e2..79f7eeb03 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -52,6 +52,7 @@ from app.models.users import UserOut from app.rabbitmq.listeners import submit_dataset_job from app.routers.authentication import get_admin, get_admin_mode +from app.routers.doi import DataCiteClient from app.routers.files import add_file_entry, add_local_file_entry from app.routers.licenses import delete_license from app.search.connect import delete_document_by_id @@ -74,8 +75,6 @@ from rocrate.model.person import Person from rocrate.rocrate import ROCrate -from app.routers.doi import DataCiteClient - router = APIRouter() security = HTTPBearer() @@ -148,12 +147,12 @@ def nested_update(target_dict, update_dict): async def _create_folder_structure( - dataset_id: str, - contents: dict, - folder_path: str, - folder_lookup: dict, - user: UserOut, - parent_folder_id: Optional[str] = None, + dataset_id: str, + contents: dict, + folder_path: str, + folder_lookup: dict, + user: UserOut, + parent_folder_id: Optional[str] = None, ): """Recursively create folders encountered in folder_path until the target folder is created. Arguments: @@ -189,10 +188,10 @@ async def _create_folder_structure( @router.post("", response_model=DatasetOut) async def save_dataset( - dataset_in: DatasetIn, - license_id: str, - user=Depends(get_current_user), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + dataset_in: DatasetIn, + license_id: str, + user=Depends(get_current_user), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): standard_license = False standard_license_ids = [license.id for license in standard_licenses] @@ -221,13 +220,13 @@ async def save_dataset( @router.get("", response_model=Paged) async def get_datasets( - user_id=Depends(get_user), - skip: int = 0, - limit: int = 10, - mine: bool = False, - admin=Depends(get_admin), - enable_admin: bool = False, - admin_mode: bool = Depends(get_admin_mode), + user_id=Depends(get_user), + skip: int = 0, + limit: int = 10, + mine: bool = False, + admin=Depends(get_admin), + enable_admin: bool = False, + admin_mode: bool = Depends(get_admin_mode), ): query = [DatasetDBViewList.frozen == False] # noqa: E712 @@ -282,18 +281,18 @@ async def get_datasets( @router.get("/{dataset_id}", response_model=DatasetOut) async def get_dataset( - dataset_id: str, - authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), - public: bool = Depends(CheckStatus("PUBLIC")), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), + public: bool = Depends(CheckStatus("PUBLIC")), + allow: bool = Depends(Authorization("viewer")), ): if authenticated or public or allow: if ( - dataset := await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), - ) + dataset := await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), ) + ) ) is not None: return dataset.dict() raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") @@ -303,24 +302,24 @@ async def get_dataset( @router.get("/{dataset_id}/files", response_model=Paged) async def get_dataset_files( - dataset_id: str, - folder_id: Optional[str] = None, - authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), - public: bool = Depends(CheckStatus("PUBLIC")), - user_id=Depends(get_user), - skip: int = 0, - limit: int = 10, - admin=Depends(get_admin), - enable_admin: bool = False, - admin_mode: bool = Depends(get_admin_mode), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + folder_id: Optional[str] = None, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), + public: bool = Depends(CheckStatus("PUBLIC")), + user_id=Depends(get_user), + skip: int = 0, + limit: int = 10, + admin=Depends(get_admin), + enable_admin: bool = False, + admin_mode: bool = Depends(get_admin_mode), + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), - ) + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), ) + ) ) is not None: if authenticated or public or (admin and admin_mode): query = [ @@ -359,11 +358,11 @@ async def get_dataset_files( @router.put("/{dataset_id}", response_model=DatasetOut) async def edit_dataset( - dataset_id: str, - dataset_info: DatasetBase, - user=Depends(get_current_user), - es=Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + dataset_info: DatasetBase, + user=Depends(get_current_user), + es=Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: # TODO: Refactor this with permissions checks etc. @@ -376,7 +375,7 @@ async def edit_dataset( # Update folders index since its using dataset downloads and status to index async for folder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id) + FolderDB.dataset_id == PydanticObjectId(dataset_id) ): await index_folder(es, FolderOut(**folder.dict()), update=True) @@ -386,11 +385,11 @@ async def edit_dataset( @router.patch("/{dataset_id}", response_model=DatasetOut) async def patch_dataset( - dataset_id: str, - dataset_info: DatasetPatch, - user=Depends(get_current_user), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + dataset_info: DatasetPatch, + user=Depends(get_current_user), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: # TODO: Update method not working properly @@ -410,7 +409,7 @@ async def patch_dataset( files_views = await FileDBViewList.find(*query).to_list() for file_view in files_views: if ( - file := await FileDB.get(PydanticObjectId(file_view.id)) + file := await FileDB.get(PydanticObjectId(file_view.id)) ) is not None: file.status = dataset_info.status await file.save() @@ -421,7 +420,7 @@ async def patch_dataset( # Update folders index since its using dataset downloads and status to index async for folder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id) + FolderDB.dataset_id == PydanticObjectId(dataset_id) ): await index_folder(es, FolderOut(**folder.dict()), update=True) @@ -430,10 +429,10 @@ async def patch_dataset( @router.delete("/{dataset_id}") async def delete_dataset( - dataset_id: str, - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: # delete from elasticsearch @@ -441,7 +440,7 @@ async def delete_dataset( # find associate frozen datasets and delete them iteratively async for frozen_dataset in DatasetFreezeDB.find( - DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id) + DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id) ): await _delete_frozen_dataset(frozen_dataset, fs, hard_delete=True) @@ -464,7 +463,7 @@ async def delete_dataset( # delete files and its associate resources async for file in FileDB.find( - FileDB.dataset_id == PydanticObjectId(dataset_id) + FileDB.dataset_id == PydanticObjectId(dataset_id) ): await remove_file_entry(file.id, fs, es) @@ -484,51 +483,56 @@ async def delete_dataset( raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") -@router.post("/{dataset_id}/doi", response_model=str) +@router.post("/{dataset_id}/doi", response_model=DatasetOut) async def mint_doi( - dataset_id: str, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization(RoleType.OWNER)), + dataset_id: str, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization(RoleType.OWNER)) and settings.DOI_ENABLED, ): - dataset = await DatasetDB.get(PydanticObjectId(dataset_id)) - if dataset is None: - return f"Dataset {dataset_id} not found" - dataset_db = dataset.dict() - metadata = { - "data": { - "type": "dois", - "attributes": { - "prefix": "10.1234", - "doi": "10.1234/your-doi-suffix", - "url": "https://your.url", - "titles": [{"title": dataset_db["name"]}], - "creators": [ - {"name": dataset_db["creator"]["first_name"]["last_name"]} - ], - "publisher": "DataCite e.V.", - "publicationYear": datetime.now().year, - }, + if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: + dataset_db = dataset.dict() + metadata = { + "data": { + "type": "dois", + "attributes": { + "prefix": os.getenv("DATACITE_PREFIX"), + "url": f"{settings.API_HOST}{settings.API_V2_STR}/datasets/{dataset_id}", + "titles": [{"title": dataset_db["name"]}], + "creators": [ + { + "name": dataset_db["creator"]["first_name"] + + dataset_db["creator"]["last_name"] + } + ], + "publisher": "DataCite e.V.", + "publicationYear": datetime.datetime.now().year, + }, + } } - } - dataCiteClient = DataCiteClient() - response = dataCiteClient.create_doi(metadata) - dataset_db["doi"] = response.json()["data"]["id"] - dataset_db["modified"] = datetime.datetime.utcnow() - await dataset_db.save() + dataCiteClient = DataCiteClient() + response = dataCiteClient.create_doi(metadata) + dataset_db["doi"] = response.get("data").get("id") + dataset_db["modified"] = datetime.datetime.utcnow() + dataset.update(dataset_db) + await dataset.save() - # Update entry to the dataset index - await index_dataset(es, DatasetOut(dataset_db), update=True) + # Update entry to the dataset index + await index_dataset(es, DatasetOut(**dataset_db), update=True) + return dataset_db + else: + raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") @router.post("/{dataset_id}/freeze", response_model=DatasetFreezeOut) async def freeze_dataset( - dataset_id: str, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization(RoleType.OWNER)), + dataset_id: str, + publish_doi: bool = False, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization(RoleType.OWNER)), ): # Retrieve the dataset by ID if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: @@ -551,7 +555,7 @@ async def freeze_dataset( frozen_dataset_data["frozen_version_num"] = 1 else: frozen_dataset_data["frozen_version_num"] = ( - latest_frozen_dataset.frozen_version_num + 1 + latest_frozen_dataset.frozen_version_num + 1 ) # start freezing associate information @@ -585,6 +589,8 @@ async def freeze_dataset( # TODO thumbnails, visualizations + await mint_doi(frozen_dataset.dict()["id"]) + return frozen_dataset.dict() raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") @@ -592,13 +598,13 @@ async def freeze_dataset( @router.get("/{dataset_id}/freeze", response_model=Paged) async def get_freeze_datasets( - dataset_id: str, - skip: int = 0, - limit: int = 10, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + skip: int = 0, + limit: int = 10, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): frozen_datasets_and_count = ( await DatasetFreezeDB.find( @@ -627,11 +633,11 @@ async def get_freeze_datasets( @router.get("/{dataset_id}/freeze/latest_version_num", response_model=int) async def get_freeze_dataset_lastest_version_num( - dataset_id: str, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): freeze_dataset_latest_version_num = -999 latest_frozen_dataset = ( @@ -651,19 +657,19 @@ async def get_freeze_dataset_lastest_version_num( "/{dataset_id}/freeze/{frozen_version_num}", response_model=DatasetFreezeOut ) async def get_freeze_dataset_version( - dataset_id: str, - frozen_version_num: int, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + frozen_version_num: int, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): # Retrieve the dataset by ID if ( - frozen_dataset := await DatasetFreezeDB.find_one( - DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), - DatasetFreezeDB.frozen_version_num == frozen_version_num, - ) + frozen_dataset := await DatasetFreezeDB.find_one( + DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), + DatasetFreezeDB.frozen_version_num == frozen_version_num, + ) ) is not None: if frozen_dataset.deleted is True: raise HTTPException( @@ -683,19 +689,19 @@ async def get_freeze_dataset_version( "/{dataset_id}/freeze/{frozen_version_num}", response_model=DatasetFreezeOut ) async def delete_freeze_dataset_version( - dataset_id: str, - frozen_version_num: int, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("owner")), + dataset_id: str, + frozen_version_num: int, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("owner")), ): # Retrieve the frozen dataset by ID if ( - frozen_dataset := await DatasetFreezeDB.find_one( - DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), - DatasetFreezeDB.frozen_version_num == frozen_version_num, - ) + frozen_dataset := await DatasetFreezeDB.find_one( + DatasetFreezeDB.origin_id == PydanticObjectId(dataset_id), + DatasetFreezeDB.frozen_version_num == frozen_version_num, + ) ) is not None: return await _delete_frozen_dataset(frozen_dataset, fs, hard_delete=False) @@ -707,11 +713,11 @@ async def delete_freeze_dataset_version( @router.post("/{dataset_id}/folders", response_model=FolderOut) async def add_folder( - dataset_id: str, - folder_in: FolderIn, - user=Depends(get_current_user), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + folder_in: FolderIn, + user=Depends(get_current_user), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("uploader")), ): if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: parent_folder = folder_in.parent_folder @@ -731,21 +737,21 @@ async def add_folder( @router.get("/{dataset_id}/folders", response_model=Paged) async def get_dataset_folders( - dataset_id: str, - parent_folder: Optional[str] = None, - user_id=Depends(get_user), - authenticated: bool = Depends(CheckStatus("authenticated")), - public: bool = Depends(CheckStatus("PUBLIC")), - skip: int = 0, - limit: int = 10, - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + parent_folder: Optional[str] = None, + user_id=Depends(get_user), + authenticated: bool = Depends(CheckStatus("authenticated")), + public: bool = Depends(CheckStatus("PUBLIC")), + skip: int = 0, + limit: int = 10, + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), - ) + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), ) + ) ) is not None: if authenticated or public: query = [ @@ -787,24 +793,24 @@ async def get_dataset_folders( @router.get("/{dataset_id}/folders_and_files", response_model=Paged) async def get_dataset_folders_and_files( - dataset_id: str, - folder_id: Optional[str] = None, - authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), - public: bool = Depends(CheckStatus("PUBLIC")), - user_id=Depends(get_user), - skip: int = 0, - limit: int = 10, - admin=Depends(get_admin), - enable_admin: bool = False, - admin_mode: bool = Depends(get_admin_mode), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + folder_id: Optional[str] = None, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), + public: bool = Depends(CheckStatus("PUBLIC")), + user_id=Depends(get_user), + skip: int = 0, + limit: int = 10, + admin=Depends(get_admin), + enable_admin: bool = False, + admin_mode: bool = Depends(get_admin_mode), + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), - ) + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), ) + ) ) is not None: if authenticated or public or (admin and admin_mode): query = [ @@ -869,11 +875,11 @@ async def get_dataset_folders_and_files( @router.delete("/{dataset_id}/folders/{folder_id}") async def delete_folder( - dataset_id: str, - folder_id: str, - fs: Minio = Depends(dependencies.get_fs), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + folder_id: str, + fs: Minio = Depends(dependencies.get_fs), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(Authorization("editor")), ): if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None: @@ -884,14 +890,14 @@ async def delete_folder( # recursively delete child folder and files async def _delete_nested_folders(parent_folder_id): while ( - await FolderDB.find_one( - FolderDB.dataset_id == ObjectId(dataset_id), - FolderDB.parent_folder == ObjectId(parent_folder_id), - ) + await FolderDB.find_one( + FolderDB.dataset_id == ObjectId(dataset_id), + FolderDB.parent_folder == ObjectId(parent_folder_id), + ) ) is not None: async for subfolder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id), - FolderDB.parent_folder == PydanticObjectId(parent_folder_id), + FolderDB.dataset_id == PydanticObjectId(dataset_id), + FolderDB.parent_folder == PydanticObjectId(parent_folder_id), ): async for file in FileDB.find(FileDB.folder_id == subfolder.id): await remove_file_entry(file.id, fs, es) @@ -910,16 +916,16 @@ async def _delete_nested_folders(parent_folder_id): @router.get("/{dataset_id}/folders/{folder_id}") async def get_folder( - dataset_id: str, - folder_id: str, - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + folder_id: str, + allow: bool = Depends(Authorization("viewer")), ): if ( - await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), - ) + await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), ) + ) ) is not None: if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None: return folder.dict() @@ -930,12 +936,12 @@ async def get_folder( @router.patch("/{dataset_id}/folders/{folder_id}", response_model=FolderOut) async def patch_folder( - dataset_id: str, - folder_id: str, - folder_info: FolderPatch, - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - user=Depends(get_current_user), - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + folder_id: str, + folder_info: FolderPatch, + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + user=Depends(get_current_user), + allow: bool = Depends(Authorization("editor")), ): if await DatasetDB.get(PydanticObjectId(dataset_id)) is not None: if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None: @@ -945,8 +951,8 @@ async def patch_folder( # allow moving folder around within the hierarchy if folder_info.parent_folder is not None: if ( - await FolderDB.get(PydanticObjectId(folder_info.parent_folder)) - is not None + await FolderDB.get(PydanticObjectId(folder_info.parent_folder)) + is not None ): folder.parent_folder = folder_info.parent_folder folder.modified = datetime.datetime.utcnow() @@ -965,14 +971,14 @@ async def patch_folder( @router.post("/{dataset_id}/files", response_model=FileOut) async def save_file( - dataset_id: str, - folder_id: Optional[str] = None, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - file: UploadFile = File(...), - es=Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + folder_id: Optional[str] = None, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + file: UploadFile = File(...), + es=Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if user is None: @@ -1017,14 +1023,14 @@ async def save_file( @router.post("/{dataset_id}/filesMultiple", response_model=List[FileOut]) async def save_files( - dataset_id: str, - files: List[UploadFile], - folder_id: Optional[str] = None, - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - es=Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + files: List[UploadFile], + folder_id: Optional[str] = None, + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + es=Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: files_added = [] @@ -1044,7 +1050,7 @@ async def save_files( if folder_id is not None: if ( - folder := await FolderDB.get(PydanticObjectId(folder_id)) + folder := await FolderDB.get(PydanticObjectId(folder_id)) ) is not None: new_file.folder_id = folder.id else: @@ -1078,13 +1084,13 @@ async def save_files( @router.post("/{dataset_id}/local_files", response_model=FileOut) async def save_local_file( - localfile_in: LocalFileIn, - dataset_id: str, - folder_id: Optional[str] = None, - user=Depends(get_current_user), - es=Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + localfile_in: LocalFileIn, + dataset_id: str, + folder_id: Optional[str] = None, + user=Depends(get_current_user), + es=Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if user is None: @@ -1133,12 +1139,12 @@ async def save_local_file( @router.post("/createFromZip", response_model=DatasetOut) async def create_dataset_from_zip( - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - file: UploadFile = File(...), - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - token: str = Depends(get_token), + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + file: UploadFile = File(...), + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + token: str = Depends(get_token), ): if file.filename.endswith(".zip") is False: raise HTTPException(status_code=404, detail="File is not a zip file") @@ -1206,16 +1212,16 @@ async def create_dataset_from_zip( @router.get("/{dataset_id}/download") async def download_dataset( - dataset_id: str, - es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), - user=Depends(get_current_user), - fs: Minio = Depends(dependencies.get_fs), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + user=Depends(get_current_user), + fs: Minio = Depends(dependencies.get_fs), + allow: bool = Depends(Authorization("viewer")), ): if ( - dataset := await DatasetDBViewList.find_one( - DatasetDBViewList.id == PydanticObjectId(dataset_id) - ) + dataset := await DatasetDBViewList.find_one( + DatasetDBViewList.id == PydanticObjectId(dataset_id) + ) ) is not None: current_temp_dir = tempfile.mkdtemp(prefix="rocratedownload") crate = ROCrate() @@ -1260,7 +1266,7 @@ async def download_dataset( file_count = 0 async for file in FileDBViewList.find( - FileDBViewList.dataset_id == ObjectId(dataset_id) + FileDBViewList.dataset_id == ObjectId(dataset_id) ): # find the bytes id # if it's working draft file_id == origin_id @@ -1376,7 +1382,7 @@ async def download_dataset( await index_dataset(es, DatasetOut(**dataset.dict()), update=True) # Update folders index since its using dataset downloads and status to index async for folder in FolderDB.find( - FolderDB.dataset_id == PydanticObjectId(dataset_id) + FolderDB.dataset_id == PydanticObjectId(dataset_id) ): await index_folder(es, FolderOut(**folder.dict()), update=True) @@ -1388,14 +1394,14 @@ async def download_dataset( # can handle parameeters pass in as key/values in info @router.post("/{dataset_id}/extract") async def get_dataset_extract( - dataset_id: str, - extractorName: str, - request: Request, - # parameters don't have a fixed model shape - parameters: dict = None, - user=Depends(get_current_user), - rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), - allow: bool = Depends(Authorization("uploader")), + dataset_id: str, + extractorName: str, + request: Request, + # parameters don't have a fixed model shape + parameters: dict = None, + user=Depends(get_current_user), + rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(Authorization("uploader")), ): if extractorName is None: raise HTTPException(status_code=400, detail="No extractorName specified") @@ -1415,17 +1421,17 @@ async def get_dataset_extract( @router.get("/{dataset_id}/thumbnail") async def download_dataset_thumbnail( - dataset_id: str, - fs: Minio = Depends(dependencies.get_fs), - allow: bool = Depends(Authorization("viewer")), + dataset_id: str, + fs: Minio = Depends(dependencies.get_fs), + allow: bool = Depends(Authorization("viewer")), ): # If dataset exists in MongoDB, download from Minio if ( - dataset := await DatasetDBViewList.find_one( - Or( - DatasetDBViewList.id == PydanticObjectId(dataset_id), - ) + dataset := await DatasetDBViewList.find_one( + Or( + DatasetDBViewList.id == PydanticObjectId(dataset_id), ) + ) ) is not None: if dataset.thumbnail_id is not None: content = fs.get_object( @@ -1448,9 +1454,9 @@ async def download_dataset_thumbnail( @router.patch("/{dataset_id}/thumbnail/{thumbnail_id}", response_model=DatasetOut) async def add_dataset_thumbnail( - dataset_id: str, - thumbnail_id: str, - allow: bool = Depends(Authorization("editor")), + dataset_id: str, + thumbnail_id: str, + allow: bool = Depends(Authorization("editor")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if (await ThumbnailDB.get(PydanticObjectId(thumbnail_id))) is not None: diff --git a/backend/app/routers/doi.py b/backend/app/routers/doi.py index cda91e62d..1dfcac3c9 100644 --- a/backend/app/routers/doi.py +++ b/backend/app/routers/doi.py @@ -1,11 +1,14 @@ -from app.config import settings -from fastapi import requests +import os + +import requests from requests.auth import HTTPBasicAuth class DataCiteClient: - def __init__(self, test_mode=True): - self.auth = HTTPBasicAuth(settings.USERNAME, settings.PASSWORD) + def __init__(self, test_mode=False): + self.auth = HTTPBasicAuth( + os.getenv("DATACITE_USERNAME"), os.getenv("DATACITE_PASSWORD") + ) self.headers = {"Content-Type": "application/vnd.api+json"} self.base_url = ( "https://api.test.datacite.org/" diff --git a/backend/app/tests/test_datasets.py b/backend/app/tests/test_datasets.py index 079d6f987..218f75176 100644 --- a/backend/app/tests/test_datasets.py +++ b/backend/app/tests/test_datasets.py @@ -23,6 +23,21 @@ def test_get_one(client: TestClient, headers: dict): assert response.json().get("id") is not None +def test_create_doi(client: TestClient, headers: dict): + dataset_id = create_dataset(client, headers).get("id") + response = client.get( + f"{settings.API_V2_STR}/datasets/{dataset_id}", headers=headers + ) + assert response.status_code == 200 + assert response.json().get("id") is not None + response = client.post( + f"{settings.API_V2_STR}/datasets/{dataset_id}/doi", headers=headers + ) + assert response.status_code == 200 + print(response.json()) + assert response.json().get("doi") is not None + + def test_delete(client: TestClient, headers: dict): dataset_id = create_dataset(client, headers).get("id") response = client.delete( diff --git a/frontend/src/app.config.ts b/frontend/src/app.config.ts index 7c325852c..8c15da95c 100644 --- a/frontend/src/app.config.ts +++ b/frontend/src/app.config.ts @@ -33,6 +33,7 @@ interface Config { defaultExtractionJobs: number; defaultMetadataDefintionPerPage: number; defaultVersionPerPage: number; + enableDOI: boolean; } const config: Config = {}; @@ -100,5 +101,6 @@ config["defaultFeeds"] = 5; config["defaultExtractionJobs"] = 5; config["defaultMetadataDefintionPerPage"] = 5; config["defaultVersionPerPage"] = 3; +config["enableDOI"] = true; export default config; diff --git a/frontend/src/openapi/v2/services/DatasetsService.ts b/frontend/src/openapi/v2/services/DatasetsService.ts index 200312fe6..340f0e73b 100644 --- a/frontend/src/openapi/v2/services/DatasetsService.ts +++ b/frontend/src/openapi/v2/services/DatasetsService.ts @@ -243,19 +243,19 @@ export class DatasetsService { /** * Mint Doi * @param datasetId - * @param enableAdmin - * @returns string Successful Response + * @param allow + * @returns DatasetOut Successful Response * @throws ApiError */ public static mintDoiApiV2DatasetsDatasetIdDoiPost( datasetId: string, - enableAdmin: boolean = false, - ): CancelablePromise { + allow: boolean = true, + ): CancelablePromise { return __request({ method: 'POST', path: `/api/v2/datasets/${datasetId}/doi`, query: { - 'enable_admin': enableAdmin, + 'allow': allow, }, errors: { 422: `Validation Error`, @@ -295,18 +295,21 @@ export class DatasetsService { /** * Freeze Dataset * @param datasetId + * @param publishDoi * @param enableAdmin * @returns DatasetFreezeOut Successful Response * @throws ApiError */ public static freezeDatasetApiV2DatasetsDatasetIdFreezePost( datasetId: string, + publishDoi: boolean = false, enableAdmin: boolean = false, ): CancelablePromise { return __request({ method: 'POST', path: `/api/v2/datasets/${datasetId}/freeze`, query: { + 'publish_doi': publishDoi, 'enable_admin': enableAdmin, }, errors: { From 466d082e408275379f8d85d4447ef988652a8299 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 30 Jul 2024 10:44:34 -0500 Subject: [PATCH 04/14] making work with frontend function --- backend/app/routers/datasets.py | 25 ++++++++----------- backend/app/tests/test_datasets.py | 6 ----- frontend/src/actions/dataset.js | 5 ++-- .../components/datasets/DatasetDetails.tsx | 4 ++- .../src/components/datasets/OtherMenu.tsx | 8 +++--- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 79f7eeb03..66b0acd2a 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -491,20 +491,16 @@ async def mint_doi( es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), allow: bool = Depends(Authorization(RoleType.OWNER)) and settings.DOI_ENABLED, ): - if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: - dataset_db = dataset.dict() + if (dataset := await DatasetFreezeDB.get(PydanticObjectId(dataset_id))) is not None: metadata = { "data": { "type": "dois", "attributes": { "prefix": os.getenv("DATACITE_PREFIX"), "url": f"{settings.API_HOST}{settings.API_V2_STR}/datasets/{dataset_id}", - "titles": [{"title": dataset_db["name"]}], + "titles": [{"title": dataset.name}], "creators": [ - { - "name": dataset_db["creator"]["first_name"] - + dataset_db["creator"]["last_name"] - } + {"name": dataset.creator.first_name + dataset.creator.last_name} ], "publisher": "DataCite e.V.", "publicationYear": datetime.datetime.now().year, @@ -513,14 +509,14 @@ async def mint_doi( } dataCiteClient = DataCiteClient() response = dataCiteClient.create_doi(metadata) - dataset_db["doi"] = response.get("data").get("id") - dataset_db["modified"] = datetime.datetime.utcnow() - dataset.update(dataset_db) + print("doi created:", response.get("data").get("id")) + dataset.doi = response.get("data").get("id") + dataset.modified = datetime.datetime.utcnow() await dataset.save() - # Update entry to the dataset index - await index_dataset(es, DatasetOut(**dataset_db), update=True) - return dataset_db + # TODO: if we ever index freeze datasets + # await index_dataset(es, DatasetOut(**dataset_db), update=True) + return dataset.dict() else: raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") @@ -589,7 +585,8 @@ async def freeze_dataset( # TODO thumbnails, visualizations - await mint_doi(frozen_dataset.dict()["id"]) + if publish_doi: + return await mint_doi(frozen_dataset.id) return frozen_dataset.dict() diff --git a/backend/app/tests/test_datasets.py b/backend/app/tests/test_datasets.py index 218f75176..2fe6f6c33 100644 --- a/backend/app/tests/test_datasets.py +++ b/backend/app/tests/test_datasets.py @@ -30,12 +30,6 @@ def test_create_doi(client: TestClient, headers: dict): ) assert response.status_code == 200 assert response.json().get("id") is not None - response = client.post( - f"{settings.API_V2_STR}/datasets/{dataset_id}/doi", headers=headers - ) - assert response.status_code == 200 - print(response.json()) - assert response.json().get("doi") is not None def test_delete(client: TestClient, headers: dict): diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index 53cb01d57..44d460a0a 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -186,10 +186,11 @@ export function updateDataset(datasetId, formData) { export const FREEZE_DATASET = "FREEZE_DATASET"; -export function freezeDataset(datasetId) { +export function freezeDataset(datasetId, publishDOI = false) { return (dispatch) => { return V2.DatasetsService.freezeDatasetApiV2DatasetsDatasetIdFreezePost( - datasetId + datasetId, + publishDOI ) .then((json) => { dispatch({ diff --git a/frontend/src/components/datasets/DatasetDetails.tsx b/frontend/src/components/datasets/DatasetDetails.tsx index 065c32186..347a31594 100644 --- a/frontend/src/components/datasets/DatasetDetails.tsx +++ b/frontend/src/components/datasets/DatasetDetails.tsx @@ -11,13 +11,15 @@ type DatasetAboutProps = { export function DatasetDetails(props: DatasetAboutProps) { const { myRole } = props; - const { id, created, modified, creator, status, downloads } = props.details; + const { id, created, modified, creator, status, downloads, doi } = + props.details; const details = new Map< string, { value: string | undefined; info?: string } >(); details.set("Owner", { value: `${creator.first_name} ${creator.last_name}` }); + if (doi != null) details.set("DOI", { value: doi }); details.set("Created", { value: parseDate(created), info: "Date and time of dataset creation", diff --git a/frontend/src/components/datasets/OtherMenu.tsx b/frontend/src/components/datasets/OtherMenu.tsx index f2f0c4c83..9def0c316 100644 --- a/frontend/src/components/datasets/OtherMenu.tsx +++ b/frontend/src/components/datasets/OtherMenu.tsx @@ -46,8 +46,10 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { const datasetRole = useSelector( (state: RootState) => state.dataset.datasetRole ); - const freezeDataset = (datasetId: string | undefined) => - dispatch(freezeDatasetAction(datasetId)); + const freezeDataset = ( + datasetId: string | undefined, + publishDOI: boolean | undefined + ) => dispatch(freezeDatasetAction(datasetId, publishDOI)); const listGroups = () => dispatch(fetchGroups(0, 21)); @@ -130,7 +132,7 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { actionText="By proceeding with the release, you will lock in the current content of the dataset, including all associated files, folders, metadata, and visualizations. Once released, these elements will be set as final and cannot be altered. However, you can continue to make edits and improvements on the ongoing version of the dataset." actionBtnName="Release" handleActionBtnClick={() => { - freezeDataset(datasetId); + freezeDataset(datasetId, true); setFreezeDatasetConfirmOpen(false); }} handleActionCancel={() => { From 97f9e6d0483794cba10c6086509cbf5486819fdc Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Tue, 30 Jul 2024 11:18:12 -0500 Subject: [PATCH 05/14] Add frontend changes for publishing DOI. --- backend/app/routers/datasets.py | 8 +- .../src/components/datasets/OtherMenu.tsx | 10 ++- .../dialog/ActionModalWithCheckbox.tsx | 82 +++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/dialog/ActionModalWithCheckbox.tsx diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 66b0acd2a..189b4b369 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -497,10 +497,14 @@ async def mint_doi( "type": "dois", "attributes": { "prefix": os.getenv("DATACITE_PREFIX"), - "url": f"{settings.API_HOST}{settings.API_V2_STR}/datasets/{dataset_id}", + "url": f"{settings.frontend_url}/datasets/{dataset_id}", "titles": [{"title": dataset.name}], "creators": [ - {"name": dataset.creator.first_name + dataset.creator.last_name} + { + "name": dataset.creator.first_name + + " " + + dataset.creator.last_name + } ], "publisher": "DataCite e.V.", "publicationYear": datetime.datetime.now().year, diff --git a/frontend/src/components/datasets/OtherMenu.tsx b/frontend/src/components/datasets/OtherMenu.tsx index 9def0c316..3874b8c7b 100644 --- a/frontend/src/components/datasets/OtherMenu.tsx +++ b/frontend/src/components/datasets/OtherMenu.tsx @@ -10,6 +10,7 @@ import { } from "@mui/material"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import { ActionModal } from "../dialog/ActionModal"; +import { ActionModalWithCheckbox } from "../dialog/ActionModalWithCheckbox"; import { datasetDeleted, freezeDataset as freezeDatasetAction, @@ -89,6 +90,7 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { }; const [anchorEl, setAnchorEl] = React.useState(null); + const [publishDOI, setPublishDOI] = React.useState(false); const handleOptionClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -126,13 +128,17 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { }} actionLevel={"error"} /> - { - freezeDataset(datasetId, true); + freezeDataset(datasetId, publishDOI); setFreezeDatasetConfirmOpen(false); }} handleActionCancel={() => { diff --git a/frontend/src/components/dialog/ActionModalWithCheckbox.tsx b/frontend/src/components/dialog/ActionModalWithCheckbox.tsx new file mode 100644 index 000000000..2b24b643c --- /dev/null +++ b/frontend/src/components/dialog/ActionModalWithCheckbox.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControlLabel, +} from "@mui/material"; + +type ActionLevel = "error" | "warning" | "info"; + +type ActionModalProps = { + actionOpen: boolean; + actionTitle: string; + actionText: string; + checkboxLabel: string; + checkboxSelected: boolean; + publishDOI: boolean; + setPublishDOI: (value: boolean) => void; + actionBtnName: string; + handleActionBtnClick: () => void; + handleActionCancel: () => void; + actionLevel?: ActionLevel; +}; + +export const ActionModalWithCheckbox: React.FC = ( + props: ActionModalProps +) => { + const { + actionOpen, + actionTitle, + actionText, + checkboxLabel, + checkboxSelected, + publishDOI, + setPublishDOI, + actionBtnName, + handleActionBtnClick, + handleActionCancel, + actionLevel, + } = props; + + return ( + + {actionTitle} + + {actionText} + { + setPublishDOI(!publishDOI); + }} + /> + } + label={checkboxLabel} + /> + + + + + + + ); +}; From f9e896b104c94a142d44f871554dfa0b1ded6dd1 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 30 Jul 2024 13:06:48 -0500 Subject: [PATCH 06/14] adding link for doi --- frontend/src/components/datasets/Dataset.tsx | 15 +++++++++++++++ .../src/components/datasets/DatasetDetails.tsx | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index d193d0a02..fb5ed3cf5 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -604,6 +604,21 @@ export const Dataset = (): JSX.Element => { ) : ( <> )} +
+ {dataset.doi && dataset.doi !== undefined ? ( +
+ + DOI + + + + https://doi.org/{dataset.doi} + + +
+ ) : ( + <> + )} diff --git a/frontend/src/components/datasets/DatasetDetails.tsx b/frontend/src/components/datasets/DatasetDetails.tsx index 347a31594..065c32186 100644 --- a/frontend/src/components/datasets/DatasetDetails.tsx +++ b/frontend/src/components/datasets/DatasetDetails.tsx @@ -11,15 +11,13 @@ type DatasetAboutProps = { export function DatasetDetails(props: DatasetAboutProps) { const { myRole } = props; - const { id, created, modified, creator, status, downloads, doi } = - props.details; + const { id, created, modified, creator, status, downloads } = props.details; const details = new Map< string, { value: string | undefined; info?: string } >(); details.set("Owner", { value: `${creator.first_name} ${creator.last_name}` }); - if (doi != null) details.set("DOI", { value: doi }); details.set("Created", { value: parseDate(created), info: "Date and time of dataset creation", From 1e0fc7214dd121bc140a5e2d4c168caa92f669d6 Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Tue, 30 Jul 2024 13:45:32 -0500 Subject: [PATCH 07/14] Do code formatting. --- .../src/components/datasets/OtherMenu.tsx | 9 ++++---- .../dialog/ActionModalWithCheckbox.tsx | 22 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/datasets/OtherMenu.tsx b/frontend/src/components/datasets/OtherMenu.tsx index 3874b8c7b..a654a4e16 100644 --- a/frontend/src/components/datasets/OtherMenu.tsx +++ b/frontend/src/components/datasets/OtherMenu.tsx @@ -131,12 +131,11 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { { freezeDataset(datasetId, publishDOI); setFreezeDatasetConfirmOpen(false); diff --git a/frontend/src/components/dialog/ActionModalWithCheckbox.tsx b/frontend/src/components/dialog/ActionModalWithCheckbox.tsx index 2b24b643c..6ac060c77 100644 --- a/frontend/src/components/dialog/ActionModalWithCheckbox.tsx +++ b/frontend/src/components/dialog/ActionModalWithCheckbox.tsx @@ -19,7 +19,7 @@ type ActionModalProps = { checkboxLabel: string; checkboxSelected: boolean; publishDOI: boolean; - setPublishDOI: (value: boolean) => void; + setCheckboxSelected: (value: boolean) => void; actionBtnName: string; handleActionBtnClick: () => void; handleActionCancel: () => void; @@ -35,8 +35,7 @@ export const ActionModalWithCheckbox: React.FC = ( actionText, checkboxLabel, checkboxSelected, - publishDOI, - setPublishDOI, + setCheckboxSelected, actionBtnName, handleActionBtnClick, handleActionCancel, @@ -48,13 +47,14 @@ export const ActionModalWithCheckbox: React.FC = ( {actionTitle} {actionText} +
{ - setPublishDOI(!publishDOI); + setCheckboxSelected(!checkboxSelected); }} /> } @@ -63,18 +63,18 @@ export const ActionModalWithCheckbox: React.FC = (
From 887e478dea657e8c0430863ff92a9a2019a51fe8 Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Tue, 30 Jul 2024 13:57:47 -0500 Subject: [PATCH 08/14] Update dataset release documentation. --- docs/docs/devs/dataset-versioning.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/docs/devs/dataset-versioning.md b/docs/docs/devs/dataset-versioning.md index 5ad91381e..7b4530b66 100644 --- a/docs/docs/devs/dataset-versioning.md +++ b/docs/docs/devs/dataset-versioning.md @@ -128,8 +128,12 @@ for client consumption. These views include: - providing users with greater control over dataset management - **Forbidden Modifications**: Prevent modifications to a released dataset. -## Future Enhancements +## Digital Object Identifier (DOI) Integration +Currently, the feature to generate DOI through [DataCite](https://datacite.org/) is integrated with Clowder. The user is provided this option +when they release a dataset. Clowder then talks with the DataCite API to mint a DOI for the released dataset and +submits some metadata about the dataset like its title, URL, and creator details. The generated DOI is displayed in the +dataset page in the Details section. -- **Mint DOI**: Integrate DOI support to allow minting Digital Object Identifiers (DOIs) for each dataset version, - ensuring unique and persistent - identification ([Issue #919](https://github.com/clowder-framework/clowder2/issues/919)). +## Future Enhancements +- **Add support for CrossRef when generate DOI**: Currently, Clowder supports DataCite for minting DOIs. We might need to + integrate CrossRef to provide users with more options, as some users may already have an account with CrossRef. From c0de34ee9b07f8c9973be8dbf85f3cbee4fcbe8a Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Tue, 30 Jul 2024 14:27:59 -0500 Subject: [PATCH 09/14] Use configuration setting to hide/display the DOI minting feature. --- frontend/src/app.config.ts | 2 ++ frontend/src/components/datasets/OtherMenu.tsx | 12 +++++++++++- .../components/dialog/ActionModalWithCheckbox.tsx | 5 ++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/app.config.ts b/frontend/src/app.config.ts index 8c15da95c..c8b6d1c18 100644 --- a/frontend/src/app.config.ts +++ b/frontend/src/app.config.ts @@ -101,6 +101,8 @@ config["defaultFeeds"] = 5; config["defaultExtractionJobs"] = 5; config["defaultMetadataDefintionPerPage"] = 5; config["defaultVersionPerPage"] = 3; + +// Use this boolean to enable/disable DOI minting feature config["enableDOI"] = true; export default config; diff --git a/frontend/src/components/datasets/OtherMenu.tsx b/frontend/src/components/datasets/OtherMenu.tsx index a654a4e16..0d6a602e1 100644 --- a/frontend/src/components/datasets/OtherMenu.tsx +++ b/frontend/src/components/datasets/OtherMenu.tsx @@ -28,6 +28,7 @@ import { AuthWrapper } from "../auth/AuthWrapper"; import { RootState } from "../../types/data"; import ShareIcon from "@mui/icons-material/Share"; import LocalOfferIcon from "@mui/icons-material/LocalOffer"; +import config from "../../app.config"; type ActionsMenuProps = { datasetId?: string; @@ -35,6 +36,7 @@ type ActionsMenuProps = { }; export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { + let doiActionText; const { datasetId, datasetName } = props; // use history hook to redirect/navigate between routes @@ -92,6 +94,13 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { const [anchorEl, setAnchorEl] = React.useState(null); const [publishDOI, setPublishDOI] = React.useState(false); + doiActionText = + "By proceeding with the release, you will lock in the current content of the dataset, including all associated files, folders, metadata, and visualizations. Once released, these elements will be set as final and cannot be altered. However, you can continue to make edits and improvements on the ongoing version of the dataset."; + if (config.enableDOI) { + doiActionText += + " Optionally, you can also generate a Digital Object Identifier (DOI) by selecting the checkbox below. It will be displayed in the dataset page in the Details section."; + } + const handleOptionClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -131,7 +140,8 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { = ( actionOpen, actionTitle, actionText, + displayCheckbox, checkboxLabel, checkboxSelected, setCheckboxSelected, @@ -47,7 +49,7 @@ export const ActionModalWithCheckbox: React.FC = ( {actionTitle} {actionText} -
+ = ( }} /> } + sx={{ display: displayCheckbox ? "block" : "none" }} label={checkboxLabel} />
From 1d105aa11c22edeade529f48f558c70f349f7717 Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Tue, 30 Jul 2024 14:29:10 -0500 Subject: [PATCH 10/14] Remove test related to DOI. --- backend/app/tests/test_datasets.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/app/tests/test_datasets.py b/backend/app/tests/test_datasets.py index 2fe6f6c33..079d6f987 100644 --- a/backend/app/tests/test_datasets.py +++ b/backend/app/tests/test_datasets.py @@ -23,15 +23,6 @@ def test_get_one(client: TestClient, headers: dict): assert response.json().get("id") is not None -def test_create_doi(client: TestClient, headers: dict): - dataset_id = create_dataset(client, headers).get("id") - response = client.get( - f"{settings.API_V2_STR}/datasets/{dataset_id}", headers=headers - ) - assert response.status_code == 200 - assert response.json().get("id") is not None - - def test_delete(client: TestClient, headers: dict): dataset_id = create_dataset(client, headers).get("id") response = client.delete( From b7148957c1a637010da30670be507bbb9fde05d3 Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Tue, 30 Jul 2024 14:45:58 -0500 Subject: [PATCH 11/14] Update documentation related to configuration details. --- backend/app/config.py | 1 + docs/docs/devs/dataset-versioning.md | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/backend/app/config.py b/backend/app/config.py index 9cc2fa8b7..93d715bbf 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -89,6 +89,7 @@ class Settings(BaseSettings): listener_heartbeat_interval = 5 * 60 # DOI datacite details + # TODO: Update the below configuration details once access to DataCite production instance is ready DOI_ENABLED = True DATACITE_TEST_URL = "https://api.test.datacite.org/dois" DATACITE_URL = "https://api.datacite.org/dois" diff --git a/docs/docs/devs/dataset-versioning.md b/docs/docs/devs/dataset-versioning.md index 7b4530b66..7f970bf8e 100644 --- a/docs/docs/devs/dataset-versioning.md +++ b/docs/docs/devs/dataset-versioning.md @@ -134,6 +134,28 @@ when they release a dataset. Clowder then talks with the DataCite API to mint a submits some metadata about the dataset like its title, URL, and creator details. The generated DOI is displayed in the dataset page in the Details section. +### DOI Configuration Details +The following configuration changes need to be made to integrate DOI generation with Clowder using DataCite: + +In the backend module, the following configurations should be set: +```python +DOI_ENABLED = True # Enable or disable DOI generation +DATACITE_TEST_URL = "https://api.test.datacite.org/dois" # DataCite test URL +DATACITE_URL = "https://api.datacite.org/dois" # DataCite production URL +``` + +Also, following environment variables should be set when running the backend module: +```shell +DATACITE_USERNAME="" +DATACITE_PASSWORD="" +DATACITE_PREFIX="" +``` + +In the frontend module, the following configuration should be set: +```javascript +config["enableDOI"] = true; // Enable or disable DOI generation +``` + ## Future Enhancements - **Add support for CrossRef when generate DOI**: Currently, Clowder supports DataCite for minting DOIs. We might need to integrate CrossRef to provide users with more options, as some users may already have an account with CrossRef. From 6c628e603334d9810967780efc317e96e5db58de Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 30 Jul 2024 15:17:50 -0500 Subject: [PATCH 12/14] adding resourType to DOI API request body --- backend/app/routers/datasets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 189b4b369..80093bdd5 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -508,6 +508,7 @@ async def mint_doi( ], "publisher": "DataCite e.V.", "publicationYear": datetime.datetime.now().year, + "types": {"resourceTypeGeneral": "Dataset"}, }, } } From a4a790456ab109af5f46897abd37eaad1646204b Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 6 Aug 2024 13:48:21 -0500 Subject: [PATCH 13/14] added publish attribute to doi metadata --- backend/app/config.py | 3 +-- backend/app/routers/datasets.py | 1 + backend/app/routers/doi.py | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 93d715bbf..e42516a5e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -91,8 +91,7 @@ class Settings(BaseSettings): # DOI datacite details # TODO: Update the below configuration details once access to DataCite production instance is ready DOI_ENABLED = True - DATACITE_TEST_URL = "https://api.test.datacite.org/dois" - DATACITE_URL = "https://api.datacite.org/dois" + DATACITE_API_URL = "https://api.test.datacite.org/" settings = Settings() diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 80093bdd5..7569d3bd1 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -495,6 +495,7 @@ async def mint_doi( metadata = { "data": { "type": "dois", + "event": "publish", "attributes": { "prefix": os.getenv("DATACITE_PREFIX"), "url": f"{settings.frontend_url}/datasets/{dataset_id}", diff --git a/backend/app/routers/doi.py b/backend/app/routers/doi.py index 1dfcac3c9..dc0c17dee 100644 --- a/backend/app/routers/doi.py +++ b/backend/app/routers/doi.py @@ -1,20 +1,17 @@ import os import requests +from app.config import settings from requests.auth import HTTPBasicAuth class DataCiteClient: - def __init__(self, test_mode=False): + def __init__(self): self.auth = HTTPBasicAuth( os.getenv("DATACITE_USERNAME"), os.getenv("DATACITE_PASSWORD") ) self.headers = {"Content-Type": "application/vnd.api+json"} - self.base_url = ( - "https://api.test.datacite.org/" - if test_mode - else "https://api.datacite.org/" - ) + self.base_url = settings.DATACITE_API_URL def create_doi(self, metadata): url = f"{self.base_url}dois" From aae8d4697c1bb90c74a3724a62e4f639f9e40b0c Mon Sep 17 00:00:00 2001 From: Sandeep Puthanveetil Satheesan Date: Thu, 22 Aug 2024 10:58:08 -0500 Subject: [PATCH 14/14] Update documentation and comments in backend config. --- backend/app/config.py | 3 +-- docs/docs/devs/dataset-versioning.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index e42516a5e..6bbea81ea 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -88,8 +88,7 @@ class Settings(BaseSettings): # defautl listener heartbeat time interval in seconds 5 minutes listener_heartbeat_interval = 5 * 60 - # DOI datacite details - # TODO: Update the below configuration details once access to DataCite production instance is ready + # DataCite details DOI_ENABLED = True DATACITE_API_URL = "https://api.test.datacite.org/" diff --git a/docs/docs/devs/dataset-versioning.md b/docs/docs/devs/dataset-versioning.md index 7f970bf8e..bf4fc5790 100644 --- a/docs/docs/devs/dataset-versioning.md +++ b/docs/docs/devs/dataset-versioning.md @@ -140,8 +140,7 @@ The following configuration changes need to be made to integrate DOI generation In the backend module, the following configurations should be set: ```python DOI_ENABLED = True # Enable or disable DOI generation -DATACITE_TEST_URL = "https://api.test.datacite.org/dois" # DataCite test URL -DATACITE_URL = "https://api.datacite.org/dois" # DataCite production URL +DATACITE_API_URL = "https://api.test.datacite.org/" # DataCite API URL (production URL is https://api.datacite.org/) ``` Also, following environment variables should be set when running the backend module: