From 5f0b94962cbad77f2545b5874b8796b03f8f8993 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 20 Feb 2024 14:13:53 -0600 Subject: [PATCH 01/17] Draft for license model --- backend/app/main.py | 10 ++++ backend/app/models/datasets.py | 1 + backend/app/models/licenses.py | 34 +++++++++++++ backend/app/routers/datasets.py | 8 +++- backend/app/routers/licenses.py | 84 +++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 backend/app/models/licenses.py create mode 100644 backend/app/routers/licenses.py diff --git a/backend/app/main.py b/backend/app/main.py index 9a4a901bc..4274651a9 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -53,6 +53,7 @@ visualization, public_visualization, thumbnails, + licenses ) # setup loggers @@ -60,6 +61,8 @@ from app.search.config import indexSettings from app.search.connect import connect_elasticsearch, create_index +from backend.app.models.licenses import LicenseDB + logger = logging.getLogger(__name__) app = FastAPI( @@ -227,6 +230,12 @@ tags=["thumbnails"], dependencies=[Depends(get_current_username)], ) +api_router.include_router( + licenses.router, + prefix="/licenses", + tags=["licenses"], + dependencies=[Depends(get_current_username)], +) api_router.include_router(status.router, prefix="/status", tags=["status"]) api_router.include_router(keycloak.router, prefix="/auth", tags=["auth"]) app.include_router(api_router, prefix=settings.API_V2_STR) @@ -271,6 +280,7 @@ async def startup_beanie(): VisualizationDataDB, ThumbnailDB, FolderFileViewList, + LicenseDB ], recreate_views=True, ) diff --git a/backend/app/models/datasets.py b/backend/app/models/datasets.py index 2a38e3e8f..902ca89bf 100644 --- a/backend/app/models/datasets.py +++ b/backend/app/models/datasets.py @@ -48,6 +48,7 @@ class DatasetDB(Document, DatasetBase): user_views: int = 0 downloads: int = 0 thumbnail_id: Optional[PydanticObjectId] = None + license_id: PydanticObjectId class Settings: name = "datasets" diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py new file mode 100644 index 000000000..e075a4a61 --- /dev/null +++ b/backend/app/models/licenses.py @@ -0,0 +1,34 @@ +from datetime import datetime +from typing import List + +from pydantic import BaseModel, Field +from beanie import Document, View, PydanticObjectId + +from backend.app.models.users import UserOut + + +class LicenseBase(BaseModel): + name: str + type: str + text: str + url: str + version: str + holders: List[UserOut] + expiration_date: datetime = Field(default_factory=datetime.utcnow) + allow_download: bool = False + +class LicenseIn(LicenseBase): + pass + +class LicenseDB(Document, LicenseBase): + creator: UserOut + created: datetime = Field(default_factory=datetime.utcnow) + modified: datetime = Field(default_factory=datetime.utcnow) + + class Settings: + name = "licenses" + +class LicenseOut(LicenseDB): + class Config: + fields = {"id": "id"} + diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 1689defc4..117699111 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -13,6 +13,9 @@ from beanie.operators import Or, And from bson import ObjectId from bson import json_util + +from backend.app.models.licenses import LicenseIn +from backend.app.routers.licenses import save_license from elasticsearch import Elasticsearch from fastapi import ( APIRouter, @@ -204,7 +207,10 @@ async def save_dataset( user=Depends(get_current_user), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): - dataset = DatasetDB(**dataset_in.dict(), creator=user) + license_in = {"name": "All Rights Reserved", "text": "", "type": "", "url": "", "version": "", "holders": [user], "expiration_date":"", "allow_downloads": True} + license = await save_license(license_in) + print(license["id"]) + dataset = DatasetDB(**dataset_in.dict(), creator=user, license_id=license["id"]) await dataset.insert() # Create authorization entry diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py new file mode 100644 index 000000000..41ba473d1 --- /dev/null +++ b/backend/app/routers/licenses.py @@ -0,0 +1,84 @@ +from datetime import datetime +from typing import Optional + +from beanie import PydanticObjectId +from beanie.operators import Or, Push, RegEx +from bson.objectid import ObjectId +from fastapi import HTTPException, Depends, APIRouter + +from app.deps.authorization_deps import AuthorizationDB, GroupAuthorization +from app.keycloak_auth import get_current_user, get_user +from app.models.authorization import RoleType +from app.models.groups import GroupOut, GroupIn, GroupDB, GroupBase, Member +from app.models.pages import _get_page_query, Paged, _construct_page_metadata +from app.models.users import UserOut, UserDB +from app.routers.authentication import get_admin_mode, get_admin + +from backend.app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase + +router = APIRouter() + + +@router.post("", response_model=LicenseOut) +async def save_license( + license_in: LicenseIn, + user=Depends(get_current_user), +): + license_db = LicenseDB(**license_in, creator=user) + await license_db.insert() + return license_db.dict() + + +@router.get("/{license_id}", response_model=GroupOut) +async def get_license( + license_id: str, + allow: bool = Depends(GroupAuthorization("viewer")), +): + if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: + return license.dict() + raise HTTPException(status_code=404, detail=f"Group {license_id} not found") + +@router.put("/{license_id}", response_model=GroupOut) +async def edit_license( + license_id: str, + license_info: LicenseBase, + user_id=Depends(get_user), + allow: bool = Depends(GroupAuthorization("editor")), +): + if (license := await GroupDB.get(PydanticObjectId(license_id))) is not None: + license_dict = dict(license_info) if license_info is not None else {} + + if len(license_dict["name"]) == 0 or len(license_dict["holders"]) == 0: + raise HTTPException( + status_code=400, + detail="License name can't be null or user list can't be empty", + ) + return + + user = await UserDB.find_one(UserDB.email == user_id) + license.creator = user.dict() + license.modified = datetime.utcnow() + license.holders = license_dict['holders'] + license.type = license_dict['text'] + license.url = license_dict['url'] + license.version = license_dict['version'] + license.allow_download = license_dict['allow_download'] + license.replace() + + return license.dict() + raise HTTPException(status_code=404, detail=f"License {license_id} not found") + + +@router.delete("/{license_id}", response_model=GroupOut) +async def delete_license( + license_id: str, + allow: bool = Depends(GroupAuthorization("owner")), +): + if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: + await license.delete() + return license.dict() # TODO: Do we need to return what we just deleted? + else: + raise HTTPException(status_code=404, detail=f"Dataset {license_id} not found") + + + From 8b351dcd37a53f06878117384b8d526e27e5eae6 Mon Sep 17 00:00:00 2001 From: toddn Date: Tue, 20 Feb 2024 16:06:59 -0600 Subject: [PATCH 02/17] fixing formatting --- backend/app/main.py | 4 ++-- backend/app/models/licenses.py | 4 +++- backend/app/routers/datasets.py | 11 ++++++++++- backend/app/routers/licenses.py | 14 ++++++-------- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 4274651a9..ec4dba82e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -53,7 +53,7 @@ visualization, public_visualization, thumbnails, - licenses + licenses, ) # setup loggers @@ -280,7 +280,7 @@ async def startup_beanie(): VisualizationDataDB, ThumbnailDB, FolderFileViewList, - LicenseDB + LicenseDB, ], recreate_views=True, ) diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py index e075a4a61..5097cf19b 100644 --- a/backend/app/models/licenses.py +++ b/backend/app/models/licenses.py @@ -17,9 +17,11 @@ class LicenseBase(BaseModel): expiration_date: datetime = Field(default_factory=datetime.utcnow) allow_download: bool = False + class LicenseIn(LicenseBase): pass + class LicenseDB(Document, LicenseBase): creator: UserOut created: datetime = Field(default_factory=datetime.utcnow) @@ -28,7 +30,7 @@ class LicenseDB(Document, LicenseBase): class Settings: name = "licenses" + class LicenseOut(LicenseDB): class Config: fields = {"id": "id"} - diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 117699111..39e43e02e 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -207,7 +207,16 @@ async def save_dataset( user=Depends(get_current_user), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): - license_in = {"name": "All Rights Reserved", "text": "", "type": "", "url": "", "version": "", "holders": [user], "expiration_date":"", "allow_downloads": True} + license_in = { + "name": "All Rights Reserved", + "text": "", + "type": "", + "url": "", + "version": "", + "holders": [user], + "expiration_date": "", + "allow_downloads": True, + } license = await save_license(license_in) print(license["id"]) dataset = DatasetDB(**dataset_in.dict(), creator=user, license_id=license["id"]) diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 41ba473d1..47fc486a7 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -38,6 +38,7 @@ async def get_license( return license.dict() raise HTTPException(status_code=404, detail=f"Group {license_id} not found") + @router.put("/{license_id}", response_model=GroupOut) async def edit_license( license_id: str, @@ -58,11 +59,11 @@ async def edit_license( user = await UserDB.find_one(UserDB.email == user_id) license.creator = user.dict() license.modified = datetime.utcnow() - license.holders = license_dict['holders'] - license.type = license_dict['text'] - license.url = license_dict['url'] - license.version = license_dict['version'] - license.allow_download = license_dict['allow_download'] + license.holders = license_dict["holders"] + license.type = license_dict["text"] + license.url = license_dict["url"] + license.version = license_dict["version"] + license.allow_download = license_dict["allow_download"] license.replace() return license.dict() @@ -79,6 +80,3 @@ async def delete_license( return license.dict() # TODO: Do we need to return what we just deleted? else: raise HTTPException(status_code=404, detail=f"Dataset {license_id} not found") - - - From 0535da1bbfcc3428cd7061680bf2ce97b73e46a1 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Wed, 21 Feb 2024 12:02:54 -0600 Subject: [PATCH 03/17] fix the license initialization --- backend/app/models/licenses.py | 17 ++++++++--------- backend/app/routers/datasets.py | 27 ++++++++++++--------------- backend/app/routers/licenses.py | 15 ++++----------- 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py index 5097cf19b..bb5d5cd85 100644 --- a/backend/app/models/licenses.py +++ b/backend/app/models/licenses.py @@ -1,10 +1,11 @@ from datetime import datetime from typing import List +from beanie import Document from pydantic import BaseModel, Field -from beanie import Document, View, PydanticObjectId -from backend.app.models.users import UserOut +from app.models.authorization import Provenance +from app.models.users import UserOut class LicenseBase(BaseModel): @@ -13,8 +14,10 @@ class LicenseBase(BaseModel): text: str url: str version: str - holders: List[UserOut] - expiration_date: datetime = Field(default_factory=datetime.utcnow) + holders: List[UserOut] = [] + expiration_date: datetime = Field( + default_factory=datetime.utcnow + ) # TODO: shoudn't the default be never? allow_download: bool = False @@ -22,11 +25,7 @@ class LicenseIn(LicenseBase): pass -class LicenseDB(Document, LicenseBase): - creator: UserOut - created: datetime = Field(default_factory=datetime.utcnow) - modified: datetime = Field(default_factory=datetime.utcnow) - +class LicenseDB(Document, LicenseBase, Provenance): class Settings: name = "licenses" diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 39e43e02e..f17a28cd7 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -13,9 +13,6 @@ from beanie.operators import Or, And from bson import ObjectId from bson import json_util - -from backend.app.models.licenses import LicenseIn -from backend.app.routers.licenses import save_license from elasticsearch import Elasticsearch from fastapi import ( APIRouter, @@ -77,6 +74,8 @@ delete_document_by_id, ) from app.search.index import index_dataset, index_file +from backend.app.models.licenses import LicenseIn +from backend.app.routers.licenses import save_license router = APIRouter() security = HTTPBearer() @@ -207,18 +206,16 @@ async def save_dataset( user=Depends(get_current_user), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): - license_in = { - "name": "All Rights Reserved", - "text": "", - "type": "", - "url": "", - "version": "", - "holders": [user], - "expiration_date": "", - "allow_downloads": True, - } - license = await save_license(license_in) - print(license["id"]) + license_in_model = LicenseIn( + name="All Rights Reserved", + text="", + type="", + url="", + version="", + holders=[user], + allow_downloads=True, + ) + license = await save_license(license_in_model, user) dataset = DatasetDB(**dataset_in.dict(), creator=user, license_id=license["id"]) await dataset.insert() diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 47fc486a7..4fad57bb9 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -1,19 +1,12 @@ from datetime import datetime -from typing import Optional from beanie import PydanticObjectId -from beanie.operators import Or, Push, RegEx -from bson.objectid import ObjectId from fastapi import HTTPException, Depends, APIRouter -from app.deps.authorization_deps import AuthorizationDB, GroupAuthorization +from app.deps.authorization_deps import GroupAuthorization from app.keycloak_auth import get_current_user, get_user -from app.models.authorization import RoleType -from app.models.groups import GroupOut, GroupIn, GroupDB, GroupBase, Member -from app.models.pages import _get_page_query, Paged, _construct_page_metadata -from app.models.users import UserOut, UserDB -from app.routers.authentication import get_admin_mode, get_admin - +from app.models.groups import GroupOut, GroupDB +from app.models.users import UserDB from backend.app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase router = APIRouter() @@ -24,7 +17,7 @@ async def save_license( license_in: LicenseIn, user=Depends(get_current_user), ): - license_db = LicenseDB(**license_in, creator=user) + license_db = LicenseDB(**license_in.dict(), creator=user.email) await license_db.insert() return license_db.dict() From f1484a67b1a2a6b27d3f9dbde0810264c296099f Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Wed, 21 Feb 2024 14:14:48 -0600 Subject: [PATCH 04/17] backend implementation --- backend/app/routers/datasets.py | 4 +-- backend/app/routers/licenses.py | 46 +++++++++++++++++++------------ backend/app/tests/test_license.py | 44 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 backend/app/tests/test_license.py diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index f17a28cd7..3bb9e1fd6 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -465,7 +465,7 @@ async def get_dataset_folders( limit: int = 10, allow: bool = Depends(Authorization("viewer")), ): - if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: + if (dataset_db := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: if authenticated or public: query = [ FolderDBViewList.dataset_id == ObjectId(dataset_id), @@ -495,7 +495,7 @@ async def get_dataset_folders( page = Paged( metadata=page_metadata, data=[ - DatasetOut(id=item.pop("_id"), **item) + DatasetOut(id=item.pop("_id"), **item, license_id=dataset_db.license_id) for item in folders_and_count[0]["data"] ], ) diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 4fad57bb9..814242186 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -5,9 +5,11 @@ from app.deps.authorization_deps import GroupAuthorization from app.keycloak_auth import get_current_user, get_user -from app.models.groups import GroupOut, GroupDB -from app.models.users import UserDB + +from backend.app.deps.authorization_deps import Authorization +from backend.app.models.users import UserDB from backend.app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase +from backend.app.routers.authentication import get_admin, get_admin_mode router = APIRouter() @@ -22,24 +24,27 @@ async def save_license( return license_db.dict() -@router.get("/{license_id}", response_model=GroupOut) -async def get_license( - license_id: str, - allow: bool = Depends(GroupAuthorization("viewer")), -): +@router.get("/{license_id}", response_model=LicenseOut) +async def get_license(license_id: str): if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: return license.dict() - raise HTTPException(status_code=404, detail=f"Group {license_id} not found") + raise HTTPException(status_code=404, detail=f"License {license_id} not found") -@router.put("/{license_id}", response_model=GroupOut) +@router.put("/{license_id}", response_model=LicenseOut) async def edit_license( license_id: str, license_info: LicenseBase, user_id=Depends(get_user), - allow: bool = Depends(GroupAuthorization("editor")), + admin=Depends(get_admin), + admin_mode: bool = Depends(get_admin_mode), ): - if (license := await GroupDB.get(PydanticObjectId(license_id))) is not None: + if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: + if license.creator != user_id and not (admin and admin_mode): + raise HTTPException( + status_code=403, + detail=f"User {user_id} doesn't have permission to edit license {license_id}", + ) license_dict = dict(license_info) if license_info is not None else {} if len(license_dict["name"]) == 0 or len(license_dict["holders"]) == 0: @@ -49,27 +54,34 @@ async def edit_license( ) return - user = await UserDB.find_one(UserDB.email == user_id) - license.creator = user.dict() license.modified = datetime.utcnow() license.holders = license_dict["holders"] - license.type = license_dict["text"] + license.type = license_dict["type"] + license.text = license_dict["text"] license.url = license_dict["url"] license.version = license_dict["version"] license.allow_download = license_dict["allow_download"] + license.name = license_dict["name"] license.replace() return license.dict() raise HTTPException(status_code=404, detail=f"License {license_id} not found") -@router.delete("/{license_id}", response_model=GroupOut) +@router.delete("/{license_id}", response_model=LicenseOut) async def delete_license( license_id: str, - allow: bool = Depends(GroupAuthorization("owner")), + user_id=Depends(get_user), + admin=Depends(get_admin), + admin_mode: bool = Depends(get_admin_mode), ): if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: + if license.creator != user_id and not (admin and admin_mode): + raise HTTPException( + status_code=403, + detail=f"User {user_id} doesn't have permission to delete license {license_id}", + ) await license.delete() return license.dict() # TODO: Do we need to return what we just deleted? else: - raise HTTPException(status_code=404, detail=f"Dataset {license_id} not found") + raise HTTPException(status_code=404, detail=f"License {license_id} not found") diff --git a/backend/app/tests/test_license.py b/backend/app/tests/test_license.py new file mode 100644 index 000000000..7013f78a7 --- /dev/null +++ b/backend/app/tests/test_license.py @@ -0,0 +1,44 @@ +from fastapi.testclient import TestClient + +from app.config import settings +from app.tests.utils import ( + create_dataset, + create_user, + user_example, +) + + +def test_license(client: TestClient, headers: dict): + # create + 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 + license_id = response.json().get("license_id") + assert license_id is not None + + # get + response = client.get( + f"{settings.API_V2_STR}/licenses/{license_id}", headers=headers + ) + assert response.status_code == 200 + + # edit + license_info = response.json() + license_info["version"] = "1.1" + license_info["text"] = "abc" + response = client.put( + f"{settings.API_V2_STR}/licenses/{license_id}", + headers=headers, + json=license_info, + ) + assert response.status_code == 200 + assert response.json()["version"] == "1.1" + assert response.json()["text"] == "abc" + + # delete + response = client.delete( + f"{settings.API_V2_STR}/licenses/{license_id}", headers=headers + ) + assert response.status_code == 200 From 0ade24c938343a4aab2211230975e43adfd52277 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 22 Feb 2024 14:21:45 -0600 Subject: [PATCH 05/17] Basic frontend implementation --- backend/app/models/datasets.py | 1 - backend/app/models/licenses.py | 3 +- backend/app/routers/datasets.py | 7 +- backend/app/routers/licenses.py | 17 +-- backend/app/tests/test_license.py | 6 +- frontend/src/actions/dataset.js | 17 +++ frontend/src/components/datasets/Dataset.tsx | 8 +- .../components/datasets/DatasetDetails.tsx | 2 +- frontend/src/openapi/v2/index.ts | 4 + frontend/src/openapi/v2/models/LicenseBase.ts | 17 +++ frontend/src/openapi/v2/models/LicenseIn.ts | 17 +++ frontend/src/openapi/v2/models/LicenseOut.ts | 34 ++++++ .../openapi/v2/services/LicensesService.ts | 100 ++++++++++++++++++ frontend/src/reducers/dataset.ts | 7 +- frontend/src/types/action.ts | 10 +- frontend/src/types/data.ts | 4 +- 16 files changed, 232 insertions(+), 22 deletions(-) create mode 100644 frontend/src/openapi/v2/models/LicenseBase.ts create mode 100644 frontend/src/openapi/v2/models/LicenseIn.ts create mode 100644 frontend/src/openapi/v2/models/LicenseOut.ts create mode 100644 frontend/src/openapi/v2/services/LicensesService.ts diff --git a/backend/app/models/datasets.py b/backend/app/models/datasets.py index 902ca89bf..2a38e3e8f 100644 --- a/backend/app/models/datasets.py +++ b/backend/app/models/datasets.py @@ -48,7 +48,6 @@ class DatasetDB(Document, DatasetBase): user_views: int = 0 downloads: int = 0 thumbnail_id: Optional[PydanticObjectId] = None - license_id: PydanticObjectId class Settings: name = "datasets" diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py index bb5d5cd85..1f4546711 100644 --- a/backend/app/models/licenses.py +++ b/backend/app/models/licenses.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import List -from beanie import Document +from beanie import Document, PydanticObjectId from pydantic import BaseModel, Field from app.models.authorization import Provenance @@ -19,6 +19,7 @@ class LicenseBase(BaseModel): default_factory=datetime.utcnow ) # TODO: shoudn't the default be never? allow_download: bool = False + dataset_id: PydanticObjectId class LicenseIn(LicenseBase): diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 3bb9e1fd6..d09c67348 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -206,6 +206,8 @@ async def save_dataset( user=Depends(get_current_user), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): + dataset = DatasetDB(**dataset_in.dict(), creator=user) + await dataset.insert() license_in_model = LicenseIn( name="All Rights Reserved", text="", @@ -214,10 +216,9 @@ async def save_dataset( version="", holders=[user], allow_downloads=True, + dataset_id=dataset.id, ) - license = await save_license(license_in_model, user) - dataset = DatasetDB(**dataset_in.dict(), creator=user, license_id=license["id"]) - await dataset.insert() + await save_license(license_in_model, user) # Create authorization entry await AuthorizationDB( diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 814242186..433837400 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -3,11 +3,8 @@ from beanie import PydanticObjectId from fastapi import HTTPException, Depends, APIRouter -from app.deps.authorization_deps import GroupAuthorization from app.keycloak_auth import get_current_user, get_user -from backend.app.deps.authorization_deps import Authorization -from backend.app.models.users import UserDB from backend.app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase from backend.app.routers.authentication import get_admin, get_admin_mode @@ -24,11 +21,17 @@ async def save_license( return license_db.dict() -@router.get("/{license_id}", response_model=LicenseOut) -async def get_license(license_id: str): - if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: +@router.get("/{dataset_id}", response_model=LicenseOut) +async def get_license(dataset_id: str): + if ( + license := await LicenseDB.find_one( + LicenseDB.dataset_id == PydanticObjectId(dataset_id) + ) + ) is not None: return license.dict() - raise HTTPException(status_code=404, detail=f"License {license_id} not found") + raise HTTPException( + status_code=404, detail=f"License not found for dataset {dataset_id}" + ) @router.put("/{license_id}", response_model=LicenseOut) diff --git a/backend/app/tests/test_license.py b/backend/app/tests/test_license.py index 7013f78a7..8d2373556 100644 --- a/backend/app/tests/test_license.py +++ b/backend/app/tests/test_license.py @@ -15,14 +15,14 @@ def test_license(client: TestClient, headers: dict): f"{settings.API_V2_STR}/datasets/{dataset_id}", headers=headers ) assert response.status_code == 200 - license_id = response.json().get("license_id") - assert license_id is not None # get response = client.get( - f"{settings.API_V2_STR}/licenses/{license_id}", headers=headers + f"{settings.API_V2_STR}/licenses/{dataset_id}", headers=headers ) assert response.status_code == 200 + license_id = response.json().get("id") + assert license_id is not None # edit license_info = response.json() diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index 28dd7bf3e..d201da7df 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -201,6 +201,23 @@ export function fetchDatasetAbout(id) { }; } +export const RECEIVE_DATASET_LICENSE = "RECEIVE_DATASET_LICENSE"; + +export function fetchDatasetLicense(dataset_id) { + return (dispatch) => { + return V2.LicensesService.getLicenseApiV2LicensesDatasetIdGet(dataset_id).then((json) => { + dispatch({ + type: RECEIVE_DATASET_LICENSE, + license: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, fetchDatasetLicense(dataset_id))); + }); + }; +} + export const RECEIVE_DATASETS = "RECEIVE_DATASETS"; export function fetchDatasets(skip = 0, limit = 21, mine = false) { diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index 8425883b8..0fb15dc11 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -14,7 +14,7 @@ import { useParams, useSearchParams } from "react-router-dom"; import { RootState } from "../../types/data"; import { useDispatch, useSelector } from "react-redux"; import { - fetchDatasetAbout, + fetchDatasetAbout, fetchDatasetLicense, fetchFoldersFilesInDataset as fetchFoldersFilesInDatasetAction, } from "../../actions/dataset"; import { fetchFolderPath } from "../../actions/folder"; @@ -86,6 +86,8 @@ export const Dataset = (): JSX.Element => { ); const listDatasetAbout = (datasetId: string | undefined) => dispatch(fetchDatasetAbout(datasetId)); + const listDatasetLicense = (datasetId: string | undefined) => + dispatch(fetchDatasetLicense(datasetId)); const listDatasetMetadata = (datasetId: string | undefined) => dispatch(fetchDatasetMetadata(datasetId)); const getMetadatDefinitions = ( @@ -123,6 +125,7 @@ export const Dataset = (): JSX.Element => { (state: RootState) => state.dataset.foldersAndFiles.data ); const adminMode = useSelector((state: RootState) => state.user.adminMode); + const license = useSelector((state: RootState) => state.dataset.license); useEffect(() => { fetchFoldersFilesInDataset( @@ -132,6 +135,7 @@ export const Dataset = (): JSX.Element => { limit ); listDatasetAbout(datasetId); + listDatasetLicense(datasetId); getFolderPath(folderId); getMetadatDefinitions(null, 0, 100); }, [searchParams, adminMode]); @@ -211,6 +215,7 @@ export const Dataset = (): JSX.Element => { setEnableAddMetadata(false); }; + // @ts-ignore return ( {/*Error Message dialogue*/} @@ -431,6 +436,7 @@ export const Dataset = (): JSX.Element => { + {license.name !== undefined? License: {license.name} : <>} diff --git a/frontend/src/components/datasets/DatasetDetails.tsx b/frontend/src/components/datasets/DatasetDetails.tsx index 04c10f202..e5b439740 100644 --- a/frontend/src/components/datasets/DatasetDetails.tsx +++ b/frontend/src/components/datasets/DatasetDetails.tsx @@ -9,7 +9,7 @@ type DatasetAboutProps = { }; export function DatasetDetails(props: DatasetAboutProps) { - const { id, created, modified, creator, status, views, downloads } = + const { id, created, modified, creator, status, views, downloads} = props.details; const details = new Map(); diff --git a/frontend/src/openapi/v2/index.ts b/frontend/src/openapi/v2/index.ts index 1932a28e2..87eeefdcb 100644 --- a/frontend/src/openapi/v2/index.ts +++ b/frontend/src/openapi/v2/index.ts @@ -44,6 +44,9 @@ export type { GroupIn } from './models/GroupIn'; export type { GroupOut } from './models/GroupOut'; export type { HTTPValidationError } from './models/HTTPValidationError'; export type { LegacyEventListenerIn } from './models/LegacyEventListenerIn'; +export type { LicenseBase } from './models/LicenseBase'; +export type { LicenseIn } from './models/LicenseIn'; +export type { LicenseOut } from './models/LicenseOut'; export type { LocalFileIn } from './models/LocalFileIn'; export type { Member } from './models/Member'; export type { MetadataAgent } from './models/MetadataAgent'; @@ -87,6 +90,7 @@ export { FilesService } from './services/FilesService'; export { FoldersService } from './services/FoldersService'; export { GroupsService } from './services/GroupsService'; export { JobsService } from './services/JobsService'; +export { LicensesService } from './services/LicensesService'; export { ListenersService } from './services/ListenersService'; export { LoginService } from './services/LoginService'; export { MetadataService } from './services/MetadataService'; diff --git a/frontend/src/openapi/v2/models/LicenseBase.ts b/frontend/src/openapi/v2/models/LicenseBase.ts new file mode 100644 index 000000000..35d181b41 --- /dev/null +++ b/frontend/src/openapi/v2/models/LicenseBase.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { UserOut } from './UserOut'; + +export type LicenseBase = { + name: string; + type: string; + text: string; + url: string; + version: string; + holders?: Array; + expiration_date?: string; + allow_download?: boolean; + dataset_id: string; +} diff --git a/frontend/src/openapi/v2/models/LicenseIn.ts b/frontend/src/openapi/v2/models/LicenseIn.ts new file mode 100644 index 000000000..e138a2e25 --- /dev/null +++ b/frontend/src/openapi/v2/models/LicenseIn.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { UserOut } from './UserOut'; + +export type LicenseIn = { + name: string; + type: string; + text: string; + url: string; + version: string; + holders?: Array; + expiration_date?: string; + allow_download?: boolean; + dataset_id: string; +} diff --git a/frontend/src/openapi/v2/models/LicenseOut.ts b/frontend/src/openapi/v2/models/LicenseOut.ts new file mode 100644 index 000000000..54259d225 --- /dev/null +++ b/frontend/src/openapi/v2/models/LicenseOut.ts @@ -0,0 +1,34 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { UserOut } from './UserOut'; + +/** + * Document Mapping class. + * + * Fields: + * + * - `id` - MongoDB document ObjectID "_id" field. + * Mapped to the PydanticObjectId class + * + * Inherited from: + * + * - Pydantic BaseModel + * - [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) + */ +export type LicenseOut = { + creator: string; + created?: string; + modified?: string; + name: string; + type: string; + text: string; + url: string; + version: string; + holders?: Array; + expiration_date?: string; + allow_download?: boolean; + dataset_id: string; + id?: string; +} diff --git a/frontend/src/openapi/v2/services/LicensesService.ts b/frontend/src/openapi/v2/services/LicensesService.ts new file mode 100644 index 000000000..45b4d0ebb --- /dev/null +++ b/frontend/src/openapi/v2/services/LicensesService.ts @@ -0,0 +1,100 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { LicenseBase } from '../models/LicenseBase'; +import type { LicenseIn } from '../models/LicenseIn'; +import type { LicenseOut } from '../models/LicenseOut'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; + +export class LicensesService { + + /** + * Save License + * @param requestBody + * @returns LicenseOut Successful Response + * @throws ApiError + */ + public static saveLicenseApiV2LicensesPost( + requestBody: LicenseIn, + ): CancelablePromise { + return __request({ + method: 'POST', + path: `/api/v2/licenses`, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get License + * @param datasetId + * @returns LicenseOut Successful Response + * @throws ApiError + */ + public static getLicenseApiV2LicensesDatasetIdGet( + datasetId: string, + ): CancelablePromise { + return __request({ + method: 'GET', + path: `/api/v2/licenses/${datasetId}`, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Edit License + * @param licenseId + * @param requestBody + * @param datasetId + * @returns LicenseOut Successful Response + * @throws ApiError + */ + public static editLicenseApiV2LicensesLicenseIdPut( + licenseId: string, + requestBody: LicenseBase, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/licenses/${licenseId}`, + query: { + 'dataset_id': datasetId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Delete License + * @param licenseId + * @param datasetId + * @returns LicenseOut Successful Response + * @throws ApiError + */ + public static deleteLicenseApiV2LicensesLicenseIdDelete( + licenseId: string, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'DELETE', + path: `/api/v2/licenses/${licenseId}`, + query: { + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} \ No newline at end of file diff --git a/frontend/src/reducers/dataset.ts b/frontend/src/reducers/dataset.ts index 8b344b5dd..0eecdcfc4 100644 --- a/frontend/src/reducers/dataset.ts +++ b/frontend/src/reducers/dataset.ts @@ -4,6 +4,7 @@ import { FOLDER_ADDED, FOLDER_UPDATED, RECEIVE_DATASET_ABOUT, + RECEIVE_DATASET_LICENSE, RECEIVE_DATASET_ROLES, RECEIVE_DATASETS, RECEIVE_FOLDERS_FILES_IN_DATASET, @@ -31,7 +32,7 @@ import { DatasetOut as Dataset, DatasetRoles, FileOut, - FolderOut, + FolderOut, LicenseOut, Paged, PageMetadata, UserOut, @@ -51,6 +52,7 @@ const defaultState: DatasetState = { newFiles: [], newFolder: {}, roles: {}, + license: {} }; const dataset = (state = defaultState, action: DataAction) => { @@ -99,6 +101,8 @@ const dataset = (state = defaultState, action: DataAction) => { }); case RECEIVE_DATASET_ABOUT: return Object.assign({}, state, { about: action.about }); + case RECEIVE_DATASET_LICENSE: + return Object.assign({}, state, { license: action.license }); case RECEIVE_DATASET_ROLE: return Object.assign({}, state, { datasetRole: action.role }); case RECEIVE_DATASET_ROLES: @@ -141,7 +145,6 @@ const dataset = (state = defaultState, action: DataAction) => { }), }, }); - default: return state; } diff --git a/frontend/src/types/action.ts b/frontend/src/types/action.ts index be9487084..ed0687567 100644 --- a/frontend/src/types/action.ts +++ b/frontend/src/types/action.ts @@ -9,7 +9,7 @@ import { FileOut as FileSummary, FileVersion, FolderOut, - GroupOut as Group, + GroupOut as Group, LicenseOut, MetadataDefinitionOut as MetadataDefinition, MetadataOut as Metadata, Paged, @@ -50,6 +50,11 @@ interface RECEIVE_DATASET_ABOUT { about: Dataset; } +interface RECEIVE_DATASET_LICENSE { + type: "RECEIVE_DATASET_LICENSE"; + license: LicenseOut; +} + interface RECEIVE_DATASET_ROLE { role: AuthorizationBase; type: "RECEIVE_DATASET_ROLE"; @@ -728,4 +733,5 @@ export type DataAction = | RESET_PUBLIC_VIS_DATA_PRESIGNED_URL | RECEIVE_FOLDERS_FILES_IN_DATASET | RECEIVE_PUBLIC_FOLDERS_FILES_IN_DATASET - | FOLDER_UPDATED; + | FOLDER_UPDATED + | RECEIVE_DATASET_LICENSE; diff --git a/frontend/src/types/data.ts b/frontend/src/types/data.ts index a7aada182..ec803682a 100644 --- a/frontend/src/types/data.ts +++ b/frontend/src/types/data.ts @@ -6,7 +6,7 @@ import { FileOut, FileVersion, FolderOut, - GroupOut, + GroupOut, LicenseOut, MetadataDefinitionOut, MetadataOut as Metadata, Paged, @@ -128,6 +128,7 @@ export interface DatasetState { about: DatasetOut; datasetRole: AuthorizationBase; roles: DatasetRoles; + license: LicenseOut; } export interface PublicDatasetState { @@ -140,6 +141,7 @@ export interface PublicDatasetState { publicDatasetRole: AuthorizationBase; publicRoles: DatasetRoles; publicFoldersAndFiles: Paged; + license: LicenseOut; } export interface ListenerState { From 74275a21fdbb950f8a2a8dc1fc5c75bc16d372c2 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 22 Feb 2024 14:30:46 -0600 Subject: [PATCH 06/17] pytest error --- backend/app/routers/licenses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 433837400..cc89e9f20 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -5,8 +5,8 @@ from app.keycloak_auth import get_current_user, get_user -from backend.app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase -from backend.app.routers.authentication import get_admin, get_admin_mode +from app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase +from app.routers.authentication import get_admin, get_admin_mode router = APIRouter() From ad1825f2ae4f1f6b228539c09c35c46ab4860cc9 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 22 Feb 2024 14:37:46 -0600 Subject: [PATCH 07/17] pytest error fix pytest failure fix pytest failure --- backend/app/main.py | 2 +- backend/app/routers/datasets.py | 6 +- frontend/src/components/datasets/Dataset.tsx | 107 +++++++++++-------- 3 files changed, 66 insertions(+), 49 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index ec4dba82e..9f016069b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -61,7 +61,7 @@ from app.search.config import indexSettings from app.search.connect import connect_elasticsearch, create_index -from backend.app.models.licenses import LicenseDB +from app.models.licenses import LicenseDB logger = logging.getLogger(__name__) diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index d09c67348..366a0b282 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -74,8 +74,8 @@ delete_document_by_id, ) from app.search.index import index_dataset, index_file -from backend.app.models.licenses import LicenseIn -from backend.app.routers.licenses import save_license +from app.models.licenses import LicenseIn +from app.routers.licenses import save_license router = APIRouter() security = HTTPBearer() @@ -496,7 +496,7 @@ async def get_dataset_folders( page = Paged( metadata=page_metadata, data=[ - DatasetOut(id=item.pop("_id"), **item, license_id=dataset_db.license_id) + DatasetOut(id=item.pop("_id"), **item) for item in folders_and_count[0]["data"] ], ) diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index 0fb15dc11..bb7e54942 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -9,7 +9,10 @@ import { Tab, Tabs, Typography, + Link, + IconButton } from "@mui/material"; +import EditIcon from '@mui/icons-material/Edit'; import { useParams, useSearchParams } from "react-router-dom"; import { RootState } from "../../types/data"; import { useDispatch, useSelector } from "react-redux"; @@ -171,6 +174,9 @@ export const Dataset = (): JSX.Element => { setSelectedTabIndex(newTabIndex); }; + const editLicense = () =>{ + }; + const handlePageChange = (_: ChangeEvent, value: number) => { const newSkip = (value - 1) * limit; setCurrPageNum(value); @@ -355,55 +361,55 @@ export const Dataset = (): JSX.Element => { {enableAddMetadata && datasetRole.role !== undefined && datasetRole.role !== "viewer" ? ( - <> - - - + - - ) : ( - <> - - - {enableAddMetadata && + + + ) : ( + <> + + + {enableAddMetadata && datasetRole.role !== undefined && datasetRole.role !== "viewer" ? ( - - ) : ( - <> - )} - - - )} + + ) : ( + <> + )} + + + )} { - {license.name !== undefined? License: {license.name} : <>} + {license.name !== undefined ? ( + <> + + {license.name} + + + + + + ) : ( + <> + )} From c07b8ea980b5e9923a88050c545144d18754829a Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 26 Feb 2024 13:17:46 -0600 Subject: [PATCH 08/17] using json schema for edit license modal --- backend/app/routers/licenses.py | 2 +- frontend/src/actions/dataset.js | 17 ++++ frontend/src/components/datasets/Dataset.tsx | 41 +++++++++- .../components/datasets/EditLicenseModal.tsx | 80 +++++++++++++++++++ frontend/src/reducers/dataset.ts | 3 + frontend/src/schema/licenseSchema.json | 38 +++++++++ frontend/src/types/action.ts | 8 +- 7 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/datasets/EditLicenseModal.tsx create mode 100644 frontend/src/schema/licenseSchema.json diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index cc89e9f20..54a5d3c73 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -65,7 +65,7 @@ async def edit_license( license.version = license_dict["version"] license.allow_download = license_dict["allow_download"] license.name = license_dict["name"] - license.replace() + await license.replace() return license.dict() raise HTTPException(status_code=404, detail=f"License {license_id} not found") diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index d201da7df..7f44cc5b2 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -218,6 +218,23 @@ export function fetchDatasetLicense(dataset_id) { }; } +export const UPDATE_DATASET_LICENSE = "UPDATE_DATASET_LICENSE"; + +export function updateDatasetLicense(licenseId, formData) { + return (dispatch) => { + return V2.LicensesService.editLicenseApiV2LicensesLicenseIdPut(licenseId, formData).then((json) => { + dispatch({ + type: UPDATE_DATASET_LICENSE, + license: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, updateDatasetLicense(licenseId, formData))); + }); + }; +} + export const RECEIVE_DATASETS = "RECEIVE_DATASETS"; export function fetchDatasets(skip = 0, limit = 21, mine = false) { diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index bb7e54942..576e52b41 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -10,9 +10,9 @@ import { Tabs, Typography, Link, - IconButton + IconButton, DialogTitle, DialogContent, Dialog } from "@mui/material"; -import EditIcon from '@mui/icons-material/Edit'; +import EditIcon from "@mui/icons-material/Edit"; import { useParams, useSearchParams } from "react-router-dom"; import { RootState } from "../../types/data"; import { useDispatch, useSelector } from "react-redux"; @@ -53,6 +53,7 @@ import { ErrorModal } from "../errors/ErrorModal"; import { Visualization } from "../visualizations/Visualization"; import VisibilityIcon from "@mui/icons-material/Visibility"; import config from "../../app.config"; +import {EditLicenseModal} from "./EditLicenseModal"; export const Dataset = (): JSX.Element => { // path parameter @@ -119,6 +120,8 @@ export const Dataset = (): JSX.Element => { const [currPageNum, setCurrPageNum] = useState(1); + const [editLicenseOpen, setEditLicenseOpen] = useState(false); + const [limit] = useState(config.defaultFolderFilePerPage); const pageMetadata = useSelector( @@ -174,7 +177,14 @@ export const Dataset = (): JSX.Element => { setSelectedTabIndex(newTabIndex); }; - const editLicense = () =>{ + const [openEditLicenseModal, setOpenEditLicenseModal] = useState(false); + + const handleOpenEditLicenseModal = () => { + setOpenEditLicenseModal(true); // Open the modal + }; + + const handleCloseEditLicenseModal = (save: boolean) => { + setOpenEditLicenseModal(false); // Close the modal }; const handlePageChange = (_: ChangeEvent, value: number) => { @@ -221,6 +231,7 @@ export const Dataset = (): JSX.Element => { setEnableAddMetadata(false); }; + // @ts-ignore // @ts-ignore return ( @@ -446,10 +457,32 @@ export const Dataset = (): JSX.Element => { <> {license.name} - + { + setEditLicenseOpen(true); + }} + > + {/**/} + {/* */} + {/**/} + { + setOpenEditLicenseModal(false); + }} + fullWidth={true} + maxWidth="md" + aria-labelledby="form-dialog" + > + Edit license + + + + + {/**/} ) : ( <> diff --git a/frontend/src/components/datasets/EditLicenseModal.tsx b/frontend/src/components/datasets/EditLicenseModal.tsx new file mode 100644 index 000000000..7ff8e0d32 --- /dev/null +++ b/frontend/src/components/datasets/EditLicenseModal.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import {Container, Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Box} from '@mui/material'; +import { LicenseOut } from './LicenseOut'; +import licenseSchema from "../../schema/licenseSchema.json"; +import {FormProps} from "@rjsf/core"; +import {ClowderRjsfErrorList} from "../styledComponents/ClowderRjsfErrorList"; +import Form from "@rjsf/material-ui"; +import {createGroup as createGroupAction} from "../../actions/group"; +import {useDispatch, useSelector} from "react-redux"; +import {useNavigate} from "react-router-dom"; +import {ClowderRjsfTextWidget} from "../styledComponents/ClowderRjsfTextWidget"; +import {ClowderRjsfTextAreaWidget} from "../styledComponents/ClowderRjsfTextAreaWidget"; +import {ClowderRjsfSelectWidget} from "../styledComponents/ClowderRjsfSelectWidget"; +import {RootState} from "../../types/data"; +import {updateDatasetLicense} from "../../actions/dataset"; // Import LicenseOut type + +// interface EditLicenseModalProps { +// license: LicenseOut; +// open: boolean; +// handleClose: (save: boolean) => void; +// } + +const widgets = { + TextWidget: ClowderRjsfTextWidget, + TextAreaWidget: ClowderRjsfTextAreaWidget, + SelectWidget: ClowderRjsfSelectWidget, +}; + +type EditLicenseModalProps = { + setEditLicenseOpen: any; +} + +export const EditLicenseModal = (props: EditLicenseModalProps)=> { + const { setEditLicenseOpen } = props; + + const dispatch = useDispatch(); + + const editLicense = (formData: FormData) => + dispatch(updateDatasetLicense(license.id, formData)); + + const license = useSelector((state: RootState) => state.dataset.license); + + const onSave = async () => { + // Handle save operation here + }; + + // Filter out the 'id' property from the license object + //const filteredLicense = Object.fromEntries(Object.entries(license).filter(([key]) => key !== 'id' && key !== 'modified' && key !== 'created' && key !== 'dataset_id' )); + + return ( + +
["schema"]} + uiSchema={licenseSchema["uiSchema"] as FormProps["uiSchema"]} + formData={license} + onSubmit={({ formData }) => { + editLicense(formData); + // close modal + setEditLicenseOpen(false); + }} + ErrorList={ClowderRjsfErrorList} + > + + + + + +
+ ); +} diff --git a/frontend/src/reducers/dataset.ts b/frontend/src/reducers/dataset.ts index 0eecdcfc4..9bf4abe2e 100644 --- a/frontend/src/reducers/dataset.ts +++ b/frontend/src/reducers/dataset.ts @@ -5,6 +5,7 @@ import { FOLDER_UPDATED, RECEIVE_DATASET_ABOUT, RECEIVE_DATASET_LICENSE, + UPDATE_DATASET_LICENSE, RECEIVE_DATASET_ROLES, RECEIVE_DATASETS, RECEIVE_FOLDERS_FILES_IN_DATASET, @@ -103,6 +104,8 @@ const dataset = (state = defaultState, action: DataAction) => { return Object.assign({}, state, { about: action.about }); case RECEIVE_DATASET_LICENSE: return Object.assign({}, state, { license: action.license }); + case UPDATE_DATASET_LICENSE: + return Object.assign({}, state, { license: action.license }); case RECEIVE_DATASET_ROLE: return Object.assign({}, state, { datasetRole: action.role }); case RECEIVE_DATASET_ROLES: diff --git a/frontend/src/schema/licenseSchema.json b/frontend/src/schema/licenseSchema.json new file mode 100644 index 000000000..deb7c72bc --- /dev/null +++ b/frontend/src/schema/licenseSchema.json @@ -0,0 +1,38 @@ +{ + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "text": { + "type": "string", + "title": "Text" + }, + "url": { + "type": "string", + "title": "Url" + }, + "version": { + "type": "string", + "title": "Version" + } + } + }, + "uiSchema": { + "name": { + "ui:autofocus": true, + "ui:emptyValue": "", + "ui:placeholder": "ui:emptyValue causes this field to always be valid despite being required", + "ui:description": "License name" + } + } +} diff --git a/frontend/src/types/action.ts b/frontend/src/types/action.ts index ed0687567..9ee0537c5 100644 --- a/frontend/src/types/action.ts +++ b/frontend/src/types/action.ts @@ -55,6 +55,11 @@ interface RECEIVE_DATASET_LICENSE { license: LicenseOut; } +interface UPDATE_DATASET_LICENSE { + type: "UPDATE_DATASET_LICENSE"; + license: LicenseOut; +} + interface RECEIVE_DATASET_ROLE { role: AuthorizationBase; type: "RECEIVE_DATASET_ROLE"; @@ -734,4 +739,5 @@ export type DataAction = | RECEIVE_FOLDERS_FILES_IN_DATASET | RECEIVE_PUBLIC_FOLDERS_FILES_IN_DATASET | FOLDER_UPDATED - | RECEIVE_DATASET_LICENSE; + | RECEIVE_DATASET_LICENSE + | UPDATE_DATASET_LICENSE; From 14bb03b2e2fa057d66ce72ebd7db40408d5252aa Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 29 Feb 2024 10:01:00 -0600 Subject: [PATCH 09/17] ready --- backend/app/models/licenses.py | 10 +++----- backend/app/routers/datasets.py | 6 ++--- backend/app/routers/licenses.py | 5 ++-- backend/app/tests/test_license.py | 4 +-- frontend/src/components/datasets/Dataset.tsx | 2 +- .../components/datasets/EditLicenseModal.tsx | 19 +------------- frontend/src/openapi/v2/models/LicenseBase.ts | 8 ++---- frontend/src/openapi/v2/models/LicenseIn.ts | 8 ++---- frontend/src/openapi/v2/models/LicenseOut.ts | 8 ++---- frontend/src/schema/licenseSchema.json | 25 ++++++++----------- 10 files changed, 28 insertions(+), 67 deletions(-) diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py index 1f4546711..fa9f1b871 100644 --- a/backend/app/models/licenses.py +++ b/backend/app/models/licenses.py @@ -10,15 +10,11 @@ class LicenseBase(BaseModel): name: str - type: str - text: str + description: str url: str version: str - holders: List[UserOut] = [] - expiration_date: datetime = Field( - default_factory=datetime.utcnow - ) # TODO: shoudn't the default be never? - allow_download: bool = False + holders: str + expiration_date: datetime = Field(None) dataset_id: PydanticObjectId diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 366a0b282..6a557c679 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -210,12 +210,10 @@ async def save_dataset( await dataset.insert() license_in_model = LicenseIn( name="All Rights Reserved", - text="", - type="", + description="", url="", version="", - holders=[user], - allow_downloads=True, + holders=user.email, dataset_id=dataset.id, ) await save_license(license_in_model, user) diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 54a5d3c73..a98ea4bef 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -59,11 +59,10 @@ async def edit_license( license.modified = datetime.utcnow() license.holders = license_dict["holders"] - license.type = license_dict["type"] - license.text = license_dict["text"] + license.description = license_dict["description"] license.url = license_dict["url"] license.version = license_dict["version"] - license.allow_download = license_dict["allow_download"] + license.expiration_date = license_dict["expiration_date"] license.name = license_dict["name"] await license.replace() diff --git a/backend/app/tests/test_license.py b/backend/app/tests/test_license.py index 8d2373556..4a4b2c13e 100644 --- a/backend/app/tests/test_license.py +++ b/backend/app/tests/test_license.py @@ -27,7 +27,7 @@ def test_license(client: TestClient, headers: dict): # edit license_info = response.json() license_info["version"] = "1.1" - license_info["text"] = "abc" + license_info["description"] = "abc" response = client.put( f"{settings.API_V2_STR}/licenses/{license_id}", headers=headers, @@ -35,7 +35,7 @@ def test_license(client: TestClient, headers: dict): ) assert response.status_code == 200 assert response.json()["version"] == "1.1" - assert response.json()["text"] == "abc" + assert response.json()["description"] == "abc" # delete response = client.delete( diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index 576e52b41..435c9397d 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -453,7 +453,7 @@ export const Dataset = (): JSX.Element => { - {license.name !== undefined ? ( + {license!== undefined && license.name !== undefined ? ( <> {license.name} diff --git a/frontend/src/components/datasets/EditLicenseModal.tsx b/frontend/src/components/datasets/EditLicenseModal.tsx index 7ff8e0d32..bd96b8c03 100644 --- a/frontend/src/components/datasets/EditLicenseModal.tsx +++ b/frontend/src/components/datasets/EditLicenseModal.tsx @@ -1,30 +1,16 @@ import React, { useState } from 'react'; import {Container, Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Box} from '@mui/material'; -import { LicenseOut } from './LicenseOut'; import licenseSchema from "../../schema/licenseSchema.json"; import {FormProps} from "@rjsf/core"; import {ClowderRjsfErrorList} from "../styledComponents/ClowderRjsfErrorList"; import Form from "@rjsf/material-ui"; -import {createGroup as createGroupAction} from "../../actions/group"; import {useDispatch, useSelector} from "react-redux"; -import {useNavigate} from "react-router-dom"; import {ClowderRjsfTextWidget} from "../styledComponents/ClowderRjsfTextWidget"; import {ClowderRjsfTextAreaWidget} from "../styledComponents/ClowderRjsfTextAreaWidget"; import {ClowderRjsfSelectWidget} from "../styledComponents/ClowderRjsfSelectWidget"; import {RootState} from "../../types/data"; import {updateDatasetLicense} from "../../actions/dataset"; // Import LicenseOut type -// interface EditLicenseModalProps { -// license: LicenseOut; -// open: boolean; -// handleClose: (save: boolean) => void; -// } - -const widgets = { - TextWidget: ClowderRjsfTextWidget, - TextAreaWidget: ClowderRjsfTextAreaWidget, - SelectWidget: ClowderRjsfSelectWidget, -}; type EditLicenseModalProps = { setEditLicenseOpen: any; @@ -44,15 +30,12 @@ export const EditLicenseModal = (props: EditLicenseModalProps)=> { // Handle save operation here }; - // Filter out the 'id' property from the license object - //const filteredLicense = Object.fromEntries(Object.entries(license).filter(([key]) => key !== 'id' && key !== 'modified' && key !== 'created' && key !== 'dataset_id' )); return (
["schema"]} - uiSchema={licenseSchema["uiSchema"] as FormProps["uiSchema"]} + //uiSchema={licenseSchema["uiSchema"] as FormProps["uiSchema"]} formData={license} onSubmit={({ formData }) => { editLicense(formData); diff --git a/frontend/src/openapi/v2/models/LicenseBase.ts b/frontend/src/openapi/v2/models/LicenseBase.ts index 35d181b41..1553f8dda 100644 --- a/frontend/src/openapi/v2/models/LicenseBase.ts +++ b/frontend/src/openapi/v2/models/LicenseBase.ts @@ -2,16 +2,12 @@ /* tslint:disable */ /* eslint-disable */ -import type { UserOut } from './UserOut'; - export type LicenseBase = { name: string; - type: string; - text: string; + description: string; url: string; version: string; - holders?: Array; + holders: string; expiration_date?: string; - allow_download?: boolean; dataset_id: string; } diff --git a/frontend/src/openapi/v2/models/LicenseIn.ts b/frontend/src/openapi/v2/models/LicenseIn.ts index e138a2e25..74cbb3368 100644 --- a/frontend/src/openapi/v2/models/LicenseIn.ts +++ b/frontend/src/openapi/v2/models/LicenseIn.ts @@ -2,16 +2,12 @@ /* tslint:disable */ /* eslint-disable */ -import type { UserOut } from './UserOut'; - export type LicenseIn = { name: string; - type: string; - text: string; + description: string; url: string; version: string; - holders?: Array; + holders: string; expiration_date?: string; - allow_download?: boolean; dataset_id: string; } diff --git a/frontend/src/openapi/v2/models/LicenseOut.ts b/frontend/src/openapi/v2/models/LicenseOut.ts index 54259d225..7b3dcbca3 100644 --- a/frontend/src/openapi/v2/models/LicenseOut.ts +++ b/frontend/src/openapi/v2/models/LicenseOut.ts @@ -2,8 +2,6 @@ /* tslint:disable */ /* eslint-disable */ -import type { UserOut } from './UserOut'; - /** * Document Mapping class. * @@ -22,13 +20,11 @@ export type LicenseOut = { created?: string; modified?: string; name: string; - type: string; - text: string; + description: string; url: string; version: string; - holders?: Array; + holders: string; expiration_date?: string; - allow_download?: boolean; dataset_id: string; id?: string; } diff --git a/frontend/src/schema/licenseSchema.json b/frontend/src/schema/licenseSchema.json index deb7c72bc..c53ae8ef8 100644 --- a/frontend/src/schema/licenseSchema.json +++ b/frontend/src/schema/licenseSchema.json @@ -9,30 +9,27 @@ "type": "string", "title": "Name" }, - "type": { + "description": { "type": "string", - "title": "Type" - }, - "text": { - "type": "string", - "title": "Text" + "title": "Description" }, "url": { "type": "string", "title": "Url" }, + "holders": { + "type": "string", + "title": "Holders" + }, "version": { "type": "string", "title": "Version" + }, + "expiration_date": { + "type": "string", + "format": "date-time", + "title": "Expiration Date" } } - }, - "uiSchema": { - "name": { - "ui:autofocus": true, - "ui:emptyValue": "", - "ui:placeholder": "ui:emptyValue causes this field to always be valid despite being required", - "ui:description": "License name" - } } } From ab1b0061d1d5031902705a82b3576604606da7f9 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 29 Feb 2024 12:37:58 -0600 Subject: [PATCH 10/17] allowing user to give empty input for expiration_date --- frontend/src/schema/licenseSchema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/schema/licenseSchema.json b/frontend/src/schema/licenseSchema.json index c53ae8ef8..7a5d2aa14 100644 --- a/frontend/src/schema/licenseSchema.json +++ b/frontend/src/schema/licenseSchema.json @@ -26,7 +26,7 @@ "title": "Version" }, "expiration_date": { - "type": "string", + "type": ["string", "null"], "format": "date-time", "title": "Expiration Date" } From fc5bc916036d9eb7924038ca77e3bb2e9bff8bec Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 7 Mar 2024 21:59:53 -0600 Subject: [PATCH 11/17] implemented standard list of licenses --- backend/app/models/datasets.py | 2 + backend/app/models/licenses.py | 50 +++++++++++++- backend/app/routers/datasets.py | 33 ++++++--- backend/app/routers/licenses.py | 34 +++++++--- backend/app/tests/test_license.py | 5 +- frontend/src/actions/dataset.js | 12 ++-- .../datasets/ChooseLicenseModal.tsx | 67 +++++++++++++++++++ .../src/components/datasets/CreateDataset.tsx | 22 +++++- frontend/src/components/datasets/Dataset.tsx | 37 +++++++--- frontend/src/openapi/v2/index.ts | 1 + frontend/src/openapi/v2/models/DatasetOut.ts | 2 + frontend/src/openapi/v2/models/LicenseBase.ts | 1 - frontend/src/openapi/v2/models/LicenseIn.ts | 1 - .../src/openapi/v2/models/LicenseOption.ts | 9 +++ frontend/src/openapi/v2/models/LicenseOut.ts | 1 - .../openapi/v2/services/DatasetsService.ts | 5 ++ .../openapi/v2/services/LicensesService.ts | 39 +++++++++-- frontend/src/styles/main.css | 22 ++++++ 18 files changed, 293 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/datasets/ChooseLicenseModal.tsx create mode 100644 frontend/src/openapi/v2/models/LicenseOption.ts diff --git a/backend/app/models/datasets.py b/backend/app/models/datasets.py index 2a38e3e8f..5fb5dd508 100644 --- a/backend/app/models/datasets.py +++ b/backend/app/models/datasets.py @@ -48,6 +48,8 @@ class DatasetDB(Document, DatasetBase): user_views: int = 0 downloads: int = 0 thumbnail_id: Optional[PydanticObjectId] = None + standard_license: bool = True + license_id: Optional[str] = None class Settings: name = "datasets" diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py index fa9f1b871..4ff575d7f 100644 --- a/backend/app/models/licenses.py +++ b/backend/app/models/licenses.py @@ -15,7 +15,6 @@ class LicenseBase(BaseModel): version: str holders: str expiration_date: datetime = Field(None) - dataset_id: PydanticObjectId class LicenseIn(LicenseBase): @@ -30,3 +29,52 @@ class Settings: class LicenseOut(LicenseDB): class Config: fields = {"id": "id"} + + +# Define a model for standard license options +class LicenseOption(BaseModel): + id: str + description: str = None + url: str = None + + +# Define standard license options +standard_licenses = [ + LicenseOption( + id="CC BY", + description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format, so long as attribution is given to the creator. The license allows for commercial use. CC BY includes the following elements:\n BY: credit must be given to the creator.", + url="https://creativecommons.org/licenses/by/4.0/", + ), + LicenseOption( + id="CC BY-SA", + description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format, so long as attribution is given to the creator. The license allows for commercial use. If you remix, adapt, or build upon the material, you must license the modified material under identical terms. CC BY-SA includes the following elements:\n BY: credit must be given to the creator. SA: Adaptations must be shared under the same terms.", + url="https://creativecommons.org/licenses/by-sa/4.0/", + ), + LicenseOption( + id="CC BY-NC", + description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format for noncommercial purposes only, and only so long as attribution is given to the creator. CC BY-NC includes the following elements:\nBY: credit must be given to the creator.NC: Only noncommercial uses of the work are permitted.", + url="https://creativecommons.org/licenses/by-nc/4.0/", + ), + LicenseOption( + id="CC BY-NC-SA", + description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format for noncommercial purposes only, and only so long as attribution is given to the creator. If you remix, adapt, or build upon the material, you must license the modified material under identical terms. CC BY-NC-SA includes the following elements:\nBY: credit must be given to the creator.\nNC: Only noncommercial uses of the work are permitted.\nSA: Adaptations must be shared under the same terms.", + url="https://creativecommons.org/licenses/by-nc-sa/4.0/", + ), + LicenseOption( + id="CC BY-ND", + description="This license enables reusers to copy and distribute the material in any medium or format in unadapted form only, and only so long as attribution is given to the creator. The license allows for commercial use. CC BY-ND includes the following elements:\nBY: credit must be given to the creator.\nND: No derivatives or adaptations of the work are permitted.", + url="https://creativecommons.org/licenses/by-nd/4.0/", + ), + LicenseOption( + id="CC BY-NC-ND", + description="This license enables reusers to copy and distribute the material in any medium or format in unadapted form only, for noncommercial purposes only, and only so long as attribution is given to the creator. CC BY-NC-ND includes the following elements:\nBY: credit must be given to the creator.\nNC: Only noncommercial uses of the work are permitted.\nND: No derivatives or adaptations of the work are permitted.", + url="https://creativecommons.org/licenses/by-nc-nd/4.0/", + ), + LicenseOption( + id="CCO Public Domain Dedication", + description="CC0 (aka CC Zero) is a public dedication tool, which enables creators to give up their copyright and put their works into the worldwide public domain. CC0 enables reusers to distribute, remix, adapt, and build upon the material in any medium or format, with no conditions.", + url="https://creativecommons.org/publicdomain/zero/1.0/", + ), + LicenseOption(id="Custom", description="Create your own custom license", url="") + # Add more standard license options as needed +] diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 6a557c679..998a401bd 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -203,20 +203,30 @@ async def _get_folder_hierarchy( @router.post("", response_model=DatasetOut) async def save_dataset( dataset_in: DatasetIn, + license_id: Optional[str] = None, user=Depends(get_current_user), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): - dataset = DatasetDB(**dataset_in.dict(), creator=user) - await dataset.insert() - license_in_model = LicenseIn( - name="All Rights Reserved", - description="", - url="", - version="", - holders=user.email, - dataset_id=dataset.id, + standard_license = True + if license_id is None or license_id == "Custom": + license_in_model = LicenseIn( + name="All Rights Reserved", + description="", + url="", + version="", + holders=user.email, + ) + license = await save_license(license_in_model, user) + license_id = license["id"] + standard_license = False + + dataset = DatasetDB( + **dataset_in.dict(), + creator=user, + license_id=str(license_id), + standard_license=standard_license, ) - await save_license(license_in_model, user) + await dataset.insert() # Create authorization entry await AuthorizationDB( @@ -425,9 +435,10 @@ async def delete_dataset( FolderDB.dataset_id == PydanticObjectId(dataset_id) ).delete() await AuthorizationDB.find( - AuthorizationDB.dataset_id == ObjectId(dataset_id) + AuthorizationDB.dataset_id == PydanticObjectId(dataset_id) ).delete() return {"deleted": dataset_id} + await delete_license(dataset.license_id) raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found") diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index a98ea4bef..1a6e1bfaf 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import List from beanie import PydanticObjectId from fastapi import HTTPException, Depends, APIRouter @@ -8,6 +9,10 @@ from app.models.licenses import LicenseOut, LicenseIn, LicenseDB, LicenseBase from app.routers.authentication import get_admin, get_admin_mode +from app.models.licenses import LicenseOption + +from app.models.licenses import standard_licenses + router = APIRouter() @@ -21,19 +26,32 @@ async def save_license( return license_db.dict() -@router.get("/{dataset_id}", response_model=LicenseOut) -async def get_license(dataset_id: str): - if ( - license := await LicenseDB.find_one( - LicenseDB.dataset_id == PydanticObjectId(dataset_id) - ) - ) is not None: +@router.get("/{license_id}", response_model=LicenseOut) +async def get_license(license_id: str): + if (license := await LicenseDB.get(PydanticObjectId(license_id))) is not None: return license.dict() raise HTTPException( - status_code=404, detail=f"License not found for dataset {dataset_id}" + status_code=404, detail=f"License not found for id {license_id}" ) +# Endpoint to retrieve standard license options +@router.get("", response_model=List[LicenseOption]) +def get_licenses(): + return standard_licenses + + +@router.get("/standard_licenses/{license_id}", response_model=str) +def get_standard_license_url(license_id: str): + for license in standard_licenses: + if license.id == license_id: + # Return the URL if the license ID is found + return license.url + + # If license ID is not found, raise HTTP 404 error + raise HTTPException(status_code=404, detail="Standard License ID not found") + + @router.put("/{license_id}", response_model=LicenseOut) async def edit_license( license_id: str, diff --git a/backend/app/tests/test_license.py b/backend/app/tests/test_license.py index 4a4b2c13e..e9868b801 100644 --- a/backend/app/tests/test_license.py +++ b/backend/app/tests/test_license.py @@ -15,14 +15,13 @@ def test_license(client: TestClient, headers: dict): f"{settings.API_V2_STR}/datasets/{dataset_id}", headers=headers ) assert response.status_code == 200 + license_id = response.json().get("license_id") # get response = client.get( - f"{settings.API_V2_STR}/licenses/{dataset_id}", headers=headers + f"{settings.API_V2_STR}/licenses/{license_id}", headers=headers ) assert response.status_code == 200 - license_id = response.json().get("id") - assert license_id is not None # edit license_info = response.json() diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index 7f44cc5b2..4266ec997 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -203,9 +203,9 @@ export function fetchDatasetAbout(id) { export const RECEIVE_DATASET_LICENSE = "RECEIVE_DATASET_LICENSE"; -export function fetchDatasetLicense(dataset_id) { +export function fetchDatasetLicense(license_id) { return (dispatch) => { - return V2.LicensesService.getLicenseApiV2LicensesDatasetIdGet(dataset_id).then((json) => { + return V2.LicensesService.getLicenseApiV2LicensesLicenseIdGet(license_id).then((json) => { dispatch({ type: RECEIVE_DATASET_LICENSE, license: json, @@ -213,7 +213,7 @@ export function fetchDatasetLicense(dataset_id) { }); }) .catch((reason) => { - dispatch(handleErrors(reason, fetchDatasetLicense(dataset_id))); + dispatch(handleErrors(reason, fetchDatasetLicense(license_id))); }); }; } @@ -256,9 +256,9 @@ export function fetchDatasets(skip = 0, limit = 21, mine = false) { export const CREATE_DATASET = "CREATE_DATASET"; -export function datasetCreated(formData) { +export function datasetCreated(formData, licenseId) { return (dispatch) => { - return V2.DatasetsService.saveDatasetApiV2DatasetsPost(formData) + return V2.DatasetsService.saveDatasetApiV2DatasetsPost(formData, licenseId) .then((dataset) => { dispatch({ type: CREATE_DATASET, @@ -267,7 +267,7 @@ export function datasetCreated(formData) { }); }) .catch((reason) => { - dispatch(handleErrors(reason, datasetCreated(formData))); + dispatch(handleErrors(reason, datasetCreated(formData, licenseId))); }); }; } diff --git a/frontend/src/components/datasets/ChooseLicenseModal.tsx b/frontend/src/components/datasets/ChooseLicenseModal.tsx new file mode 100644 index 000000000..c68e60d7e --- /dev/null +++ b/frontend/src/components/datasets/ChooseLicenseModal.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from "react"; +import {Box, Button, FormControl, FormControlLabel, Link, Radio, RadioGroup} from "@mui/material"; +import { V2 } from "../../openapi"; +import { LicenseOption } from "../../openapi/v2"; + +type ChooseLicenseModalProps = { + setSelectedLicense: any; + handleBack: any; + handleNext: any +}; + +export const ChooseLicenseModal: React.FC = ( + props: ChooseLicenseModalProps +) => { + const { setSelectedLicense, handleBack, handleNext } = props; + const [standardLicenses, setStandardLicenses] = useState([]); + + useEffect(() => { + V2.LicensesService.getLicensesApiV2LicensesGet() + .then((response) => setStandardLicenses(response)) + .catch((error) => console.error("Error fetching licenses:", error)); + }, []); + + const handleChange = (event: React.ChangeEvent) => { + setSelectedLicense(event.target.id); + }; + + // @ts-ignore + return ( + + {standardLicenses.map((license) => ( +
+
+ +
+
+ +
{license.id}
+ +
+ {license.description.split('\n').map((line, index) => ( + + {line} +
+
+ ))} +
+
+
+ ))} + + + + +
+ ); +}; diff --git a/frontend/src/components/datasets/CreateDataset.tsx b/frontend/src/components/datasets/CreateDataset.tsx index ce66c69ae..45bb5cdf0 100644 --- a/frontend/src/components/datasets/CreateDataset.tsx +++ b/frontend/src/components/datasets/CreateDataset.tsx @@ -23,6 +23,8 @@ import { datasetCreated, resetDatsetCreated } from "../../actions/dataset"; import { useNavigate } from "react-router-dom"; import Layout from "../Layout"; import { ErrorModal } from "../errors/ErrorModal"; +import {V2} from "../../openapi"; +import {ChooseLicenseModal} from "./ChooseLicenseModal"; export const CreateDataset = (): JSX.Element => { const dispatch = useDispatch(); @@ -36,8 +38,8 @@ export const CreateDataset = (): JSX.Element => { datasetId: string | undefined, metadata: MetadataIn ) => dispatch(postDatasetMetadata(datasetId, metadata)); - const createDataset = (formData: FormData) => - dispatch(datasetCreated(formData)); + const createDataset = (formData: FormData, licenseId: string| undefined) => + dispatch(datasetCreated(formData, licenseId)); const newDataset = useSelector( (state: RootState) => state.dataset.newDataset ); @@ -54,6 +56,7 @@ export const CreateDataset = (): JSX.Element => { const [datasetRequestForm, setdatasetRequestForm] = useState({}); const [metadataRequestForms, setMetadataRequestForms] = useState({}); const [allowSubmit, setAllowSubmit] = React.useState(false); + const [selectedLicense, setSelectedLicense] = useState(""); const history = useNavigate(); @@ -122,8 +125,9 @@ export const CreateDataset = (): JSX.Element => { // finish button post dataset; dataset ID triggers metadata posting const handleFinish = () => { + console.log("selectedLicense:" , selectedLicense); // create dataset - createDataset(datasetRequestForm); + createDataset(datasetRequestForm, selectedLicense); }; useEffect(() => { @@ -164,6 +168,18 @@ export const CreateDataset = (): JSX.Element => { + + Standard License Options + + + You can choose a license from the standard options or create your own + + + + + + + {/*step 2 Metadata*/} Required Metadata diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index 435c9397d..73ce30891 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -24,7 +24,7 @@ import { fetchFolderPath } from "../../actions/folder"; import { a11yProps, TabPanel } from "../tabs/TabComponent"; import FilesTable from "../files/FilesTable"; -import { MetadataIn } from "../../openapi/v2"; +import {LicenseOption, MetadataIn} from "../../openapi/v2"; import { DisplayMetadata } from "../metadata/DisplayMetadata"; import { DisplayListenerMetadata } from "../metadata/DisplayListenerMetadata"; import { EditMetadata } from "../metadata/EditMetadata"; @@ -54,6 +54,8 @@ import { Visualization } from "../visualizations/Visualization"; import VisibilityIcon from "@mui/icons-material/Visibility"; import config from "../../app.config"; import {EditLicenseModal} from "./EditLicenseModal"; +import {TRUE} from "ol/functions"; +import {V2} from "../../openapi"; export const Dataset = (): JSX.Element => { // path parameter @@ -90,8 +92,9 @@ export const Dataset = (): JSX.Element => { ); const listDatasetAbout = (datasetId: string | undefined) => dispatch(fetchDatasetAbout(datasetId)); - const listDatasetLicense = (datasetId: string | undefined) => - dispatch(fetchDatasetLicense(datasetId)); + const listDatasetLicense = (licenseId: string | undefined) => + dispatch(fetchDatasetLicense(licenseId)); + const listDatasetMetadata = (datasetId: string | undefined) => dispatch(fetchDatasetMetadata(datasetId)); const getMetadatDefinitions = ( @@ -132,6 +135,8 @@ export const Dataset = (): JSX.Element => { ); const adminMode = useSelector((state: RootState) => state.user.adminMode); const license = useSelector((state: RootState) => state.dataset.license); + const [standardLicenseUrl, setStandardLicenseUrl] = useState(""); + useEffect(() => { fetchFoldersFilesInDataset( @@ -141,10 +146,16 @@ export const Dataset = (): JSX.Element => { limit ); listDatasetAbout(datasetId); - listDatasetLicense(datasetId); + if (about.standard_license && about.license_id) { + V2.LicensesService.getStandardLicenseUrlApiV2LicensesStandardLicensesLicenseIdGet(about.license_id) + .then((response) => setStandardLicenseUrl(response)) + .catch((error) => console.error("Error fetching standard license url:", error)); + } + if (!about.standard_license && about.license_id) + listDatasetLicense(about.license_id); getFolderPath(folderId); getMetadatDefinitions(null, 0, 100); - }, [searchParams, adminMode]); + }, [searchParams, adminMode, about.license_id]); // for breadcrumb useEffect(() => { @@ -453,8 +464,15 @@ export const Dataset = (): JSX.Element => { - {license!== undefined && license.name !== undefined ? ( - <> + {about.standard_license && about.license_id !== undefined ? ( + + {about.license_id} + + ) : ( + <> + )} + {!about.standard_license && license!== undefined && license.name !== undefined ? ( +
{license.name} { - {/**/} - {/* */} - {/**/} { @@ -483,7 +498,7 @@ export const Dataset = (): JSX.Element => { {/**/} - +
) : ( <> )} diff --git a/frontend/src/openapi/v2/index.ts b/frontend/src/openapi/v2/index.ts index 87eeefdcb..0aeaafe35 100644 --- a/frontend/src/openapi/v2/index.ts +++ b/frontend/src/openapi/v2/index.ts @@ -46,6 +46,7 @@ export type { HTTPValidationError } from './models/HTTPValidationError'; export type { LegacyEventListenerIn } from './models/LegacyEventListenerIn'; export type { LicenseBase } from './models/LicenseBase'; export type { LicenseIn } from './models/LicenseIn'; +export type { LicenseOption } from './models/LicenseOption'; export type { LicenseOut } from './models/LicenseOut'; export type { LocalFileIn } from './models/LocalFileIn'; export type { Member } from './models/Member'; diff --git a/frontend/src/openapi/v2/models/DatasetOut.ts b/frontend/src/openapi/v2/models/DatasetOut.ts index 30c691705..aca7f3b52 100644 --- a/frontend/src/openapi/v2/models/DatasetOut.ts +++ b/frontend/src/openapi/v2/models/DatasetOut.ts @@ -28,4 +28,6 @@ export type DatasetOut = { user_views?: number; downloads?: number; thumbnail_id?: string; + standard_license?: boolean; + license_id?: string; } diff --git a/frontend/src/openapi/v2/models/LicenseBase.ts b/frontend/src/openapi/v2/models/LicenseBase.ts index 1553f8dda..a426a9172 100644 --- a/frontend/src/openapi/v2/models/LicenseBase.ts +++ b/frontend/src/openapi/v2/models/LicenseBase.ts @@ -9,5 +9,4 @@ export type LicenseBase = { version: string; holders: string; expiration_date?: string; - dataset_id: string; } diff --git a/frontend/src/openapi/v2/models/LicenseIn.ts b/frontend/src/openapi/v2/models/LicenseIn.ts index 74cbb3368..da727084a 100644 --- a/frontend/src/openapi/v2/models/LicenseIn.ts +++ b/frontend/src/openapi/v2/models/LicenseIn.ts @@ -9,5 +9,4 @@ export type LicenseIn = { version: string; holders: string; expiration_date?: string; - dataset_id: string; } diff --git a/frontend/src/openapi/v2/models/LicenseOption.ts b/frontend/src/openapi/v2/models/LicenseOption.ts new file mode 100644 index 000000000..b632a2a9c --- /dev/null +++ b/frontend/src/openapi/v2/models/LicenseOption.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type LicenseOption = { + id: string; + description?: string; + url?: string; +} diff --git a/frontend/src/openapi/v2/models/LicenseOut.ts b/frontend/src/openapi/v2/models/LicenseOut.ts index 7b3dcbca3..17aa6159c 100644 --- a/frontend/src/openapi/v2/models/LicenseOut.ts +++ b/frontend/src/openapi/v2/models/LicenseOut.ts @@ -25,6 +25,5 @@ export type LicenseOut = { version: string; holders: string; expiration_date?: string; - dataset_id: string; id?: string; } diff --git a/frontend/src/openapi/v2/services/DatasetsService.ts b/frontend/src/openapi/v2/services/DatasetsService.ts index 5f14b189e..923066b12 100644 --- a/frontend/src/openapi/v2/services/DatasetsService.ts +++ b/frontend/src/openapi/v2/services/DatasetsService.ts @@ -52,15 +52,20 @@ export class DatasetsService { /** * Save Dataset * @param requestBody + * @param licenseId * @returns DatasetOut Successful Response * @throws ApiError */ public static saveDatasetApiV2DatasetsPost( requestBody: DatasetIn, + licenseId?: string, ): CancelablePromise { return __request({ method: 'POST', path: `/api/v2/datasets`, + query: { + 'license_id': licenseId, + }, body: requestBody, mediaType: 'application/json', errors: { diff --git a/frontend/src/openapi/v2/services/LicensesService.ts b/frontend/src/openapi/v2/services/LicensesService.ts index 45b4d0ebb..b3e45f3ea 100644 --- a/frontend/src/openapi/v2/services/LicensesService.ts +++ b/frontend/src/openapi/v2/services/LicensesService.ts @@ -3,12 +3,25 @@ /* eslint-disable */ import type { LicenseBase } from '../models/LicenseBase'; import type { LicenseIn } from '../models/LicenseIn'; +import type { LicenseOption } from '../models/LicenseOption'; import type { LicenseOut } from '../models/LicenseOut'; import type { CancelablePromise } from '../core/CancelablePromise'; import { request as __request } from '../core/request'; export class LicensesService { + /** + * Get Licenses + * @returns LicenseOption Successful Response + * @throws ApiError + */ + public static getLicensesApiV2LicensesGet(): CancelablePromise> { + return __request({ + method: 'GET', + path: `/api/v2/licenses`, + }); + } + /** * Save License * @param requestBody @@ -31,16 +44,16 @@ export class LicensesService { /** * Get License - * @param datasetId + * @param licenseId * @returns LicenseOut Successful Response * @throws ApiError */ - public static getLicenseApiV2LicensesDatasetIdGet( - datasetId: string, + public static getLicenseApiV2LicensesLicenseIdGet( + licenseId: string, ): CancelablePromise { return __request({ method: 'GET', - path: `/api/v2/licenses/${datasetId}`, + path: `/api/v2/licenses/${licenseId}`, errors: { 422: `Validation Error`, }, @@ -97,4 +110,22 @@ export class LicensesService { }); } + /** + * Get Standard License Url + * @param licenseId + * @returns string Successful Response + * @throws ApiError + */ + public static getStandardLicenseUrlApiV2LicensesStandardLicensesLicenseIdGet( + licenseId: string, + ): CancelablePromise { + return __request({ + method: 'GET', + path: `/api/v2/licenses/standard_licenses/${licenseId}`, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css index 426dd2dd7..8e465427b 100644 --- a/frontend/src/styles/main.css +++ b/frontend/src/styles/main.css @@ -137,3 +137,25 @@ mark { .show-more-less-clickable { color: #F7941E; } + +.license-item { + max-width: 800px; + cursor: pointer; + margin-top: 0; + margin-bottom: 15px; + display: flex; +} +.radio-checkbox { + margin-right: .5em; + min-width: 17px; +} +.header { + font-weight: 700; + font-size: 1.28571429em; +} +.description { + color: rgba(0,0,0,.6); + font-size: smaller; + line-height: 1.4285em; +} + From 3a72e74b8d1a1a4f25489388927a75ef89eb561a Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 11 Mar 2024 13:57:16 -0500 Subject: [PATCH 12/17] added a create license modal and modified UI --- backend/app/models/licenses.py | 16 +-- backend/app/routers/datasets.py | 21 +-- backend/app/routers/licenses.py | 11 +- backend/app/tests/test_license.py | 17 ++- backend/app/tests/utils.py | 5 +- frontend/src/actions/dataset.js | 76 +++++++---- .../datasets/ChooseLicenseModal.tsx | 128 ++++++++++++------ .../src/components/datasets/CreateDataset.tsx | 13 +- .../datasets/CreateLicenseModal.tsx | 52 +++++++ frontend/src/components/datasets/Dataset.tsx | 6 +- frontend/src/openapi/v2/models/LicenseBase.ts | 8 +- frontend/src/openapi/v2/models/LicenseIn.ts | 8 +- frontend/src/openapi/v2/models/LicenseOut.ts | 8 +- .../openapi/v2/services/DatasetsService.ts | 4 +- frontend/src/schema/licenseSchema.json | 10 +- 15 files changed, 258 insertions(+), 125 deletions(-) create mode 100644 frontend/src/components/datasets/CreateLicenseModal.tsx diff --git a/backend/app/models/licenses.py b/backend/app/models/licenses.py index 4ff575d7f..21234d8dc 100644 --- a/backend/app/models/licenses.py +++ b/backend/app/models/licenses.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List +from typing import List, Optional from beanie import Document, PydanticObjectId from pydantic import BaseModel, Field @@ -10,11 +10,11 @@ class LicenseBase(BaseModel): name: str - description: str - url: str - version: str - holders: str - expiration_date: datetime = Field(None) + description: str = None + url: str = None + version: Optional[str] = None + holders: Optional[str] = None + expiration_date: Optional[datetime] = Field(None) class LicenseIn(LicenseBase): @@ -47,12 +47,12 @@ class LicenseOption(BaseModel): ), LicenseOption( id="CC BY-SA", - description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format, so long as attribution is given to the creator. The license allows for commercial use. If you remix, adapt, or build upon the material, you must license the modified material under identical terms. CC BY-SA includes the following elements:\n BY: credit must be given to the creator. SA: Adaptations must be shared under the same terms.", + description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format, so long as attribution is given to the creator. The license allows for commercial use. If you remix, adapt, or build upon the material, you must license the modified material under identical terms. CC BY-SA includes the following elements:\n BY: credit must be given to the creator. \nSA: Adaptations must be shared under the same terms.", url="https://creativecommons.org/licenses/by-sa/4.0/", ), LicenseOption( id="CC BY-NC", - description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format for noncommercial purposes only, and only so long as attribution is given to the creator. CC BY-NC includes the following elements:\nBY: credit must be given to the creator.NC: Only noncommercial uses of the work are permitted.", + description="This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format for noncommercial purposes only, and only so long as attribution is given to the creator. CC BY-NC includes the following elements:\nBY: credit must be given to the creator.\nNC: Only noncommercial uses of the work are permitted.", url="https://creativecommons.org/licenses/by-nc/4.0/", ), LicenseOption( diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 998a401bd..ca103cd30 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -74,8 +74,7 @@ delete_document_by_id, ) from app.search.index import index_dataset, index_file -from app.models.licenses import LicenseIn -from app.routers.licenses import save_license +from app.models.licenses import standard_licenses router = APIRouter() security = HTTPBearer() @@ -203,22 +202,14 @@ async def _get_folder_hierarchy( @router.post("", response_model=DatasetOut) async def save_dataset( dataset_in: DatasetIn, - license_id: Optional[str] = None, + license_id: str, user=Depends(get_current_user), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), ): - standard_license = True - if license_id is None or license_id == "Custom": - license_in_model = LicenseIn( - name="All Rights Reserved", - description="", - url="", - version="", - holders=user.email, - ) - license = await save_license(license_in_model, user) - license_id = license["id"] - standard_license = False + standard_license = False + standard_license_ids = [license.id for license in standard_licenses] + if license_id in standard_license_ids: + standard_license = True dataset = DatasetDB( **dataset_in.dict(), diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 1a6e1bfaf..694071e03 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -21,6 +21,8 @@ async def save_license( license_in: LicenseIn, user=Depends(get_current_user), ): + if license_in.holders == None: + license_in = user license_db = LicenseDB(**license_in.dict(), creator=user.email) await license_db.insert() return license_db.dict() @@ -68,10 +70,15 @@ async def edit_license( ) license_dict = dict(license_info) if license_info is not None else {} - if len(license_dict["name"]) == 0 or len(license_dict["holders"]) == 0: + if ( + len(license_dict["name"]) == 0 + or len(license_dict["description"]) == 0 + or len(license_dict["url"]) == 0 + or len(license_dict["holders"]) == 0 + ): raise HTTPException( status_code=400, - detail="License name can't be null or user list can't be empty", + detail="License name/description/url/holders can't be null or empty", ) return diff --git a/backend/app/tests/test_license.py b/backend/app/tests/test_license.py index e9868b801..159065d04 100644 --- a/backend/app/tests/test_license.py +++ b/backend/app/tests/test_license.py @@ -8,14 +8,23 @@ ) +license_example = { + "name": "test license", + "description": "test description", + "url": "test url", + "holders": " test holders", +} + + def test_license(client: TestClient, headers: dict): # create - dataset_id = create_dataset(client, headers).get("id") - response = client.get( - f"{settings.API_V2_STR}/datasets/{dataset_id}", headers=headers + response = client.post( + f"{settings.API_V2_STR}/licenses/?user=test@test.org", + headers=headers, + json=license_example, ) assert response.status_code == 200 - license_id = response.json().get("license_id") + license_id = response.json().get("id") # get response = client.get( diff --git a/backend/app/tests/utils.py b/backend/app/tests/utils.py index a81fc1730..8d8f42ef6 100644 --- a/backend/app/tests/utils.py +++ b/backend/app/tests/utils.py @@ -140,8 +140,11 @@ def create_group(client: TestClient, headers: dict): def create_dataset(client: TestClient, headers: dict): """Creates a test dataset and returns the JSON.""" + license_id = "CC BY" response = client.post( - f"{settings.API_V2_STR}/datasets", headers=headers, json=dataset_example + f"{settings.API_V2_STR}/datasets/?license_id={license_id}", + headers=headers, + json=dataset_example, ) assert response.status_code == 200 assert response.json().get("id") is not None diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index 4266ec997..071cc87f3 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -206,12 +206,12 @@ export const RECEIVE_DATASET_LICENSE = "RECEIVE_DATASET_LICENSE"; export function fetchDatasetLicense(license_id) { return (dispatch) => { return V2.LicensesService.getLicenseApiV2LicensesLicenseIdGet(license_id).then((json) => { - dispatch({ - type: RECEIVE_DATASET_LICENSE, - license: json, - receivedAt: Date.now(), - }); - }) + dispatch({ + type: RECEIVE_DATASET_LICENSE, + license: json, + receivedAt: Date.now(), + }); + }) .catch((reason) => { dispatch(handleErrors(reason, fetchDatasetLicense(license_id))); }); @@ -223,12 +223,12 @@ export const UPDATE_DATASET_LICENSE = "UPDATE_DATASET_LICENSE"; export function updateDatasetLicense(licenseId, formData) { return (dispatch) => { return V2.LicensesService.editLicenseApiV2LicensesLicenseIdPut(licenseId, formData).then((json) => { - dispatch({ - type: UPDATE_DATASET_LICENSE, - license: json, - receivedAt: Date.now(), - }); - }) + dispatch({ + type: UPDATE_DATASET_LICENSE, + license: json, + receivedAt: Date.now(), + }); + }) .catch((reason) => { dispatch(handleErrors(reason, updateDatasetLicense(licenseId, formData))); }); @@ -256,22 +256,52 @@ export function fetchDatasets(skip = 0, limit = 21, mine = false) { export const CREATE_DATASET = "CREATE_DATASET"; -export function datasetCreated(formData, licenseId) { +export function datasetCreated(formData, licenseId, licenseFormData) { return (dispatch) => { - return V2.DatasetsService.saveDatasetApiV2DatasetsPost(formData, licenseId) - .then((dataset) => { - dispatch({ - type: CREATE_DATASET, - dataset: dataset, - receivedAt: Date.now(), + if (licenseFormData) { + // If licenseFormData is present, save the license first + return V2.LicensesService.saveLicenseApiV2LicensesPost(formData) + .then((license) => { + licenseId = license.id; + console.log("license: ", licenseId); + // After saving the license, save the dataset + return V2.DatasetsService.saveDatasetApiV2DatasetsPost(licenseId, formData); + }) + .then((dataset) => { + dispatch({ + type: CREATE_DATASET, + dataset: dataset, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, datasetCreated(formData, licenseId, licenseFormData))); }); - }) - .catch((reason) => { - dispatch(handleErrors(reason, datasetCreated(formData, licenseId))); - }); + } else { + // If licenseFormData is not present, directly save the dataset + return V2.DatasetsService.saveDatasetApiV2DatasetsPost(licenseId, formData) + .then((dataset) => { + dispatch({ + type: CREATE_DATASET, + dataset: dataset, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, datasetCreated(formData, licenseId, licenseFormData))); + }); + } }; } +export function licenseCreated(formData) { + try { + return V2.LicensesService.saveLicenseApiV2LicensesPost(formData); + } catch (reason) { + (handleErrors(reason, licenseCreated(formData))); + } +} + export const RESET_CREATE_DATASET = "RESET_CREATE_DATASET"; export function resetDatsetCreated() { diff --git a/frontend/src/components/datasets/ChooseLicenseModal.tsx b/frontend/src/components/datasets/ChooseLicenseModal.tsx index c68e60d7e..c309ded06 100644 --- a/frontend/src/components/datasets/ChooseLicenseModal.tsx +++ b/frontend/src/components/datasets/ChooseLicenseModal.tsx @@ -1,67 +1,105 @@ import React, { useEffect, useState } from "react"; -import {Box, Button, FormControl, FormControlLabel, Link, Radio, RadioGroup} from "@mui/material"; +import { + Box, + Button, Dialog, + DialogContent, + DialogTitle, + FormControl, + Link +} from "@mui/material"; +import CircleIcon from '@mui/icons-material/Circle'; import { V2 } from "../../openapi"; import { LicenseOption } from "../../openapi/v2"; +import {CreateLicenseModal} from "./CreateLicenseModal"; type ChooseLicenseModalProps = { + selectedLicense: any; setSelectedLicense: any; + setLicenseRequestForm: any; handleBack: any; handleNext: any }; export const ChooseLicenseModal: React.FC = ( - props: ChooseLicenseModalProps + props: ChooseLicenseModalProps ) => { - const { setSelectedLicense, handleBack, handleNext } = props; - const [standardLicenses, setStandardLicenses] = useState([]); + const { selectedLicense, setSelectedLicense, setLicenseRequestForm, handleBack, handleNext } = props; + const [standardLicenses, setStandardLicenses] = useState([]); + const [licenseModalOpen, setLicenseModalOpen] = useState(false); - useEffect(() => { - V2.LicensesService.getLicensesApiV2LicensesGet() - .then((response) => setStandardLicenses(response)) - .catch((error) => console.error("Error fetching licenses:", error)); - }, []); + useEffect(() => { + V2.LicensesService.getLicensesApiV2LicensesGet() + .then((response) => setStandardLicenses(response)) + .catch((error) => console.error("Error fetching licenses:", error)); + }, []); - const handleChange = (event: React.ChangeEvent) => { - setSelectedLicense(event.target.id); - }; + useEffect(() => { + setSelectedLicense(); + }, []); - // @ts-ignore + const handleChange = (event: React.ChangeEvent) => { + setSelectedLicense(event.target.id); + if (event.target.id == "Custom") + setLicenseModalOpen(true); + else + setLicenseRequestForm(null); + }; + + const handleCloseModal = () => { + setLicenseModalOpen(false); + setSelectedLicense(null); // Reset selected license + }; + + // @ts-ignore return ( - - {standardLicenses.map((license) => ( -
-
- -
-
- -
{license.id}
- -
- {license.description.split('\n').map((line, index) => ( - - {line} -
-
- ))} + + {standardLicenses.map((license) => ( +
+
+ +
+
+ +
{license.id}
+ +
+ {license.description.split("\n").map((line, index) => ( + + {index > 0 ?
  • {line}
  • : line} +
    +
    + ))} +
    -
    - ))} - - - + - - + + + ); }; diff --git a/frontend/src/components/datasets/CreateDataset.tsx b/frontend/src/components/datasets/CreateDataset.tsx index 45bb5cdf0..f702739fb 100644 --- a/frontend/src/components/datasets/CreateDataset.tsx +++ b/frontend/src/components/datasets/CreateDataset.tsx @@ -19,7 +19,7 @@ import { postDatasetMetadata, } from "../../actions/metadata"; import { MetadataIn } from "../../openapi/v2"; -import { datasetCreated, resetDatsetCreated } from "../../actions/dataset"; +import {datasetCreated, licenseCreated, resetDatsetCreated} from "../../actions/dataset"; import { useNavigate } from "react-router-dom"; import Layout from "../Layout"; import { ErrorModal } from "../errors/ErrorModal"; @@ -38,8 +38,9 @@ export const CreateDataset = (): JSX.Element => { datasetId: string | undefined, metadata: MetadataIn ) => dispatch(postDatasetMetadata(datasetId, metadata)); - const createDataset = (formData: FormData, licenseId: string| undefined) => - dispatch(datasetCreated(formData, licenseId)); + const createDataset = (formData: FormData, licenseId: string| undefined, licenseFormData: FormData) => + dispatch(datasetCreated(formData, licenseId, licenseFormData)); + const newDataset = useSelector( (state: RootState) => state.dataset.newDataset ); @@ -54,6 +55,7 @@ export const CreateDataset = (): JSX.Element => { const [errorOpen, setErrorOpen] = useState(false); const [datasetRequestForm, setdatasetRequestForm] = useState({}); + const [licenseRequestForm, setLicenseRequestForm] = useState({}); const [metadataRequestForms, setMetadataRequestForms] = useState({}); const [allowSubmit, setAllowSubmit] = React.useState(false); const [selectedLicense, setSelectedLicense] = useState(""); @@ -125,9 +127,8 @@ export const CreateDataset = (): JSX.Element => { // finish button post dataset; dataset ID triggers metadata posting const handleFinish = () => { - console.log("selectedLicense:" , selectedLicense); // create dataset - createDataset(datasetRequestForm, selectedLicense); + createDataset(datasetRequestForm, selectedLicense, licenseRequestForm); }; useEffect(() => { @@ -175,7 +176,7 @@ export const CreateDataset = (): JSX.Element => { You can choose a license from the standard options or create your own - + diff --git a/frontend/src/components/datasets/CreateLicenseModal.tsx b/frontend/src/components/datasets/CreateLicenseModal.tsx new file mode 100644 index 000000000..6f84f617e --- /dev/null +++ b/frontend/src/components/datasets/CreateLicenseModal.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import {Container, Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Box} from "@mui/material"; +import licenseSchema from "../../schema/licenseSchema.json"; +import {FormProps} from "@rjsf/core"; +import {ClowderRjsfErrorList} from "../styledComponents/ClowderRjsfErrorList"; +import Form from "@rjsf/material-ui"; +import {V2} from "../../openapi"; // Import LicenseOut type + + +type CreateLicenseModalProps = { + setLicenseModalOpen: any; + setLicenseRequestForm: any; + handleNext: any; +} + +export const CreateLicenseModal = (props: CreateLicenseModalProps)=> { + const { setLicenseModalOpen, setLicenseRequestForm, handleNext } = props; + + const addLicense = (formData: FormData) => { + setLicenseRequestForm(formData); + handleNext(); + }; + + + return ( + + ["schema"]} + onSubmit={({ formData }) => { + addLicense(formData); + // close modal + setLicenseModalOpen(false); + }} + ErrorList={ClowderRjsfErrorList} + > + + + + + + + ); +}; diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index 73ce30891..6f8da1693 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -463,7 +463,9 @@ export const Dataset = (): JSX.Element => { )} - + + License + {about.standard_license && about.license_id !== undefined ? ( {about.license_id} @@ -497,11 +499,11 @@ export const Dataset = (): JSX.Element => { - {/**/}
    ) : ( <> )} + diff --git a/frontend/src/openapi/v2/models/LicenseBase.ts b/frontend/src/openapi/v2/models/LicenseBase.ts index a426a9172..5bcc95772 100644 --- a/frontend/src/openapi/v2/models/LicenseBase.ts +++ b/frontend/src/openapi/v2/models/LicenseBase.ts @@ -4,9 +4,9 @@ export type LicenseBase = { name: string; - description: string; - url: string; - version: string; - holders: string; + description?: string; + url?: string; + version?: string; + holders?: string; expiration_date?: string; } diff --git a/frontend/src/openapi/v2/models/LicenseIn.ts b/frontend/src/openapi/v2/models/LicenseIn.ts index da727084a..a7b814244 100644 --- a/frontend/src/openapi/v2/models/LicenseIn.ts +++ b/frontend/src/openapi/v2/models/LicenseIn.ts @@ -4,9 +4,9 @@ export type LicenseIn = { name: string; - description: string; - url: string; - version: string; - holders: string; + description?: string; + url?: string; + version?: string; + holders?: string; expiration_date?: string; } diff --git a/frontend/src/openapi/v2/models/LicenseOut.ts b/frontend/src/openapi/v2/models/LicenseOut.ts index 17aa6159c..bc65f3bc4 100644 --- a/frontend/src/openapi/v2/models/LicenseOut.ts +++ b/frontend/src/openapi/v2/models/LicenseOut.ts @@ -20,10 +20,10 @@ export type LicenseOut = { created?: string; modified?: string; name: string; - description: string; - url: string; - version: string; - holders: string; + description?: string; + url?: string; + version?: string; + holders?: string; expiration_date?: string; id?: string; } diff --git a/frontend/src/openapi/v2/services/DatasetsService.ts b/frontend/src/openapi/v2/services/DatasetsService.ts index 923066b12..586b26b6d 100644 --- a/frontend/src/openapi/v2/services/DatasetsService.ts +++ b/frontend/src/openapi/v2/services/DatasetsService.ts @@ -51,14 +51,14 @@ export class DatasetsService { /** * Save Dataset - * @param requestBody * @param licenseId + * @param requestBody * @returns DatasetOut Successful Response * @throws ApiError */ public static saveDatasetApiV2DatasetsPost( + licenseId: string, requestBody: DatasetIn, - licenseId?: string, ): CancelablePromise { return __request({ method: 'POST', diff --git a/frontend/src/schema/licenseSchema.json b/frontend/src/schema/licenseSchema.json index 7a5d2aa14..eb93df817 100644 --- a/frontend/src/schema/licenseSchema.json +++ b/frontend/src/schema/licenseSchema.json @@ -2,7 +2,7 @@ "schema": { "type": "object", "required": [ - "name" + "name", "description", "url", "holders" ], "properties": { "name": { @@ -10,19 +10,19 @@ "title": "Name" }, "description": { - "type": "string", + "type": ["string", "null"], "title": "Description" }, "url": { - "type": "string", + "type": ["string", "null"], "title": "Url" }, "holders": { - "type": "string", + "type": ["string", "null"], "title": "Holders" }, "version": { - "type": "string", + "type": ["string", "null"], "title": "Version" }, "expiration_date": { From 87d3782272e8c9e87d2a60b68e745fea429fec8b Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 11 Mar 2024 14:20:03 -0500 Subject: [PATCH 13/17] fix small bug --- frontend/src/actions/dataset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index 071cc87f3..1392bb240 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -260,7 +260,7 @@ export function datasetCreated(formData, licenseId, licenseFormData) { return (dispatch) => { if (licenseFormData) { // If licenseFormData is present, save the license first - return V2.LicensesService.saveLicenseApiV2LicensesPost(formData) + return V2.LicensesService.saveLicenseApiV2LicensesPost(licenseFormData) .then((license) => { licenseId = license.id; console.log("license: ", licenseId); From a066b28c6cd9a1881ac9f03c0a5c4d81e5bd2e69 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 11 Mar 2024 15:24:59 -0500 Subject: [PATCH 14/17] added standard licenses logo --- .../components/datasets/ChooseLicenseModal.tsx | 14 +++++++++----- frontend/src/components/datasets/Dataset.tsx | 4 +++- frontend/src/styles/images/CC BY-NC-ND.png | Bin 0 -> 20988 bytes frontend/src/styles/images/CC BY-NC-SA.png | Bin 0 -> 19392 bytes frontend/src/styles/images/CC BY-NC.png | Bin 0 -> 16240 bytes frontend/src/styles/images/CC BY-ND.png | Bin 0 -> 15875 bytes frontend/src/styles/images/CC BY-SA.png | Bin 0 -> 16074 bytes frontend/src/styles/images/CC BY.png | Bin 0 -> 14566 bytes .../images/CCO Public Domain Dedication.png | Bin 0 -> 13662 bytes frontend/src/styles/main.css | 5 +++++ 10 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 frontend/src/styles/images/CC BY-NC-ND.png create mode 100644 frontend/src/styles/images/CC BY-NC-SA.png create mode 100644 frontend/src/styles/images/CC BY-NC.png create mode 100644 frontend/src/styles/images/CC BY-ND.png create mode 100644 frontend/src/styles/images/CC BY-SA.png create mode 100644 frontend/src/styles/images/CC BY.png create mode 100644 frontend/src/styles/images/CCO Public Domain Dedication.png diff --git a/frontend/src/components/datasets/ChooseLicenseModal.tsx b/frontend/src/components/datasets/ChooseLicenseModal.tsx index c309ded06..e314eee3f 100644 --- a/frontend/src/components/datasets/ChooseLicenseModal.tsx +++ b/frontend/src/components/datasets/ChooseLicenseModal.tsx @@ -66,9 +66,12 @@ export const ChooseLicenseModal: React.FC = ( />
    - -
    {license.id}
    - +
    + + {`${license.id}`}/ + +
    {license.description.split("\n").map((line, index) => ( @@ -89,12 +92,13 @@ export const ChooseLicenseModal: React.FC = ( > Create custom license - +
    - - {`${license.id}`}/ - + {`${license.id}`}/ + } + {!license.url && {license.id}}
    {license.description.split("\n").map((line, index) => ( diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index 56872084c..aa7a7f475 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -54,8 +54,8 @@ import { Visualization } from "../visualizations/Visualization"; import VisibilityIcon from "@mui/icons-material/Visibility"; import config from "../../app.config"; import {EditLicenseModal} from "./EditLicenseModal"; -import {TRUE} from "ol/functions"; import {V2} from "../../openapi"; +import {fetchStandardLicenses, fetchStandardLicenseUrl} from "../../utils/licenses"; export const Dataset = (): JSX.Element => { // path parameter @@ -136,6 +136,14 @@ export const Dataset = (): JSX.Element => { const adminMode = useSelector((state: RootState) => state.user.adminMode); const license = useSelector((state: RootState) => state.dataset.license); const [standardLicenseUrl, setStandardLicenseUrl] = useState(""); + const fetchStandardLicenseUrlData = async (license_id: string) => { + try { + const data = await fetchStandardLicenseUrl(license_id); // Call your function to fetch licenses + setStandardLicenseUrl(data); // Update state with the fetched data + } catch (error) { + console.error('Error fetching license url', error); + } + }; useEffect(() => { @@ -147,9 +155,7 @@ export const Dataset = (): JSX.Element => { ); listDatasetAbout(datasetId); if (about.standard_license && about.license_id) { - V2.LicensesService.getStandardLicenseUrlApiV2LicensesStandardLicensesLicenseIdGet(about.license_id) - .then((response) => setStandardLicenseUrl(response)) - .catch((error) => console.error("Error fetching standard license url:", error)); + fetchStandardLicenseUrlData(about.license_id); } if (!about.standard_license && about.license_id) listDatasetLicense(about.license_id); @@ -469,7 +475,7 @@ export const Dataset = (): JSX.Element => { {about.standard_license && about.license_id !== undefined ? ( - {about.license_id}/ + {about.license_id}/ ) : ( @@ -478,7 +484,7 @@ export const Dataset = (): JSX.Element => { {!about.standard_license && license!== undefined && license.name !== undefined ? (
    - {license.name} + {license.name} { setEditLicenseOpen(true); diff --git a/frontend/src/openapi/v2/services/LicensesService.ts b/frontend/src/openapi/v2/services/LicensesService.ts index b3e45f3ea..5c107d26d 100644 --- a/frontend/src/openapi/v2/services/LicensesService.ts +++ b/frontend/src/openapi/v2/services/LicensesService.ts @@ -10,18 +10,6 @@ import { request as __request } from '../core/request'; export class LicensesService { - /** - * Get Licenses - * @returns LicenseOption Successful Response - * @throws ApiError - */ - public static getLicensesApiV2LicensesGet(): CancelablePromise> { - return __request({ - method: 'GET', - path: `/api/v2/licenses`, - }); - } - /** * Save License * @param requestBody @@ -110,6 +98,18 @@ export class LicensesService { }); } + /** + * Get Standard Licenses + * @returns LicenseOption Successful Response + * @throws ApiError + */ + public static getStandardLicensesApiV2LicensesStandardLicensesAllGet(): CancelablePromise> { + return __request({ + method: 'GET', + path: `/api/v2/licenses/standard_licenses/all`, + }); + } + /** * Get Standard License Url * @param licenseId diff --git a/frontend/src/styles/images/CC BY-NC-ND.png b/frontend/src/public/CC BY-NC-ND.png similarity index 100% rename from frontend/src/styles/images/CC BY-NC-ND.png rename to frontend/src/public/CC BY-NC-ND.png diff --git a/frontend/src/styles/images/CC BY-NC-SA.png b/frontend/src/public/CC BY-NC-SA.png similarity index 100% rename from frontend/src/styles/images/CC BY-NC-SA.png rename to frontend/src/public/CC BY-NC-SA.png diff --git a/frontend/src/styles/images/CC BY-NC.png b/frontend/src/public/CC BY-NC.png similarity index 100% rename from frontend/src/styles/images/CC BY-NC.png rename to frontend/src/public/CC BY-NC.png diff --git a/frontend/src/styles/images/CC BY-ND.png b/frontend/src/public/CC BY-ND.png similarity index 100% rename from frontend/src/styles/images/CC BY-ND.png rename to frontend/src/public/CC BY-ND.png diff --git a/frontend/src/styles/images/CC BY-SA.png b/frontend/src/public/CC BY-SA.png similarity index 100% rename from frontend/src/styles/images/CC BY-SA.png rename to frontend/src/public/CC BY-SA.png diff --git a/frontend/src/styles/images/CC BY.png b/frontend/src/public/CC BY.png similarity index 100% rename from frontend/src/styles/images/CC BY.png rename to frontend/src/public/CC BY.png diff --git a/frontend/src/styles/images/CCO Public Domain Dedication.png b/frontend/src/public/CCO Public Domain Dedication.png similarity index 100% rename from frontend/src/styles/images/CCO Public Domain Dedication.png rename to frontend/src/public/CCO Public Domain Dedication.png diff --git a/frontend/src/schema/licenseSchema.json b/frontend/src/schema/licenseSchema.json index eb93df817..5cac9b4cf 100644 --- a/frontend/src/schema/licenseSchema.json +++ b/frontend/src/schema/licenseSchema.json @@ -10,15 +10,15 @@ "title": "Name" }, "description": { - "type": ["string", "null"], + "type": ["string"], "title": "Description" }, "url": { - "type": ["string", "null"], + "type": ["string"], "title": "Url" }, "holders": { - "type": ["string", "null"], + "type": ["string"], "title": "Holders" }, "version": { diff --git a/frontend/src/utils/licenses.js b/frontend/src/utils/licenses.js new file mode 100644 index 000000000..0fb93d91e --- /dev/null +++ b/frontend/src/utils/licenses.js @@ -0,0 +1,19 @@ +import {V2} from "../openapi"; + +export async function fetchStandardLicenses() { + try { + return await V2.LicensesService.getStandardLicensesApiV2LicensesStandardLicensesAllGet(); + } catch (reason) { + console.error("Failed to standard licenses option: ", reason); + return {}; + } +} + +export async function fetchStandardLicenseUrl(licenseId) { + try { + return await V2.LicensesService.getStandardLicenseUrlApiV2LicensesStandardLicensesLicenseIdGet(licenseId); + } catch (reason) { + console.error("Failed to fetch standard license url: ", reason); + return {}; + } +} From 47166c080180b3d1d7451ac889bfc2f3eef37a62 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Fri, 22 Mar 2024 11:18:33 -0500 Subject: [PATCH 17/17] removing console log --- frontend/src/actions/dataset.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index 1392bb240..4ef298091 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -263,7 +263,6 @@ export function datasetCreated(formData, licenseId, licenseFormData) { return V2.LicensesService.saveLicenseApiV2LicensesPost(licenseFormData) .then((license) => { licenseId = license.id; - console.log("license: ", licenseId); // After saving the license, save the dataset return V2.DatasetsService.saveDatasetApiV2DatasetsPost(licenseId, formData); })