diff --git a/lib/tool_shed/test/base/populators.py b/lib/tool_shed/test/base/populators.py index 01cc8eea6937..652917ad0865 100644 --- a/lib/tool_shed/test/base/populators.py +++ b/lib/tool_shed/test/base/populators.py @@ -23,6 +23,7 @@ Category, CreateCategoryRequest, CreateRepositoryRequest, + DetailedRepository, from_legacy_install_info, GetInstallInfoRequest, GetOrderedInstallableRevisionsRequest, @@ -41,6 +42,7 @@ ResetMetadataOnRepositoryResponse, ToolSearchRequest, ToolSearchResults, + UpdateRepositoryRequest, Version, ) from .api_util import ( @@ -209,6 +211,18 @@ def upload_revision_raw( ) return response + def update_raw(self, repository: HasRepositoryId, request: UpdateRepositoryRequest) -> requests.Response: + repository_id = self._repository_id(repository) + put_response = self._api_interactor.put( + f"repositories/{repository_id}", json=request.model_dump(exclude_unset=True, by_alias=True) + ) + return put_response + + def update(self, repository: HasRepositoryId, request: UpdateRepositoryRequest) -> Repository: + response = self.update_raw(repository, request) + api_asserts.assert_status_code_is_ok(response) + return Repository(**response.json()) + def upload_revision( self, repository: HasRepositoryId, path: Traversable, commit_message: str = DEFAULT_COMMIT_MESSAGE ) -> RepositoryUpdate: @@ -358,11 +372,14 @@ def unset_deprecated(self, repository: HasRepositoryId): delete_response = self._api_interactor.delete(f"repositories/{repository_id}/deprecated") delete_response.raise_for_status() - def is_deprecated(self, repository: HasRepositoryId) -> bool: + def get_repository(self, repository: HasRepositoryId) -> DetailedRepository: repository_id = self._repository_id(repository) repository_response = self._api_interactor.get(f"repositories/{repository_id}") repository_response.raise_for_status() - return Repository(**repository_response.json()).deprecated + return DetailedRepository(**repository_response.json()) + + def is_deprecated(self, repository: HasRepositoryId) -> bool: + return self.get_repository(repository).deprecated def get_metadata(self, repository: HasRepositoryId, downloadable_only=True) -> RepositoryMetadata: repository_id = self._repository_id(repository) diff --git a/lib/tool_shed/test/functional/test_shed_repositories.py b/lib/tool_shed/test/functional/test_shed_repositories.py index fe7c4d3bac24..673aaa1d1fe7 100644 --- a/lib/tool_shed/test/functional/test_shed_repositories.py +++ b/lib/tool_shed/test/functional/test_shed_repositories.py @@ -10,7 +10,10 @@ HasRepositoryId, repo_tars, ) -from tool_shed_client.schema import RepositoryRevisionMetadata +from tool_shed_client.schema import ( + RepositoryRevisionMetadata, + UpdateRepositoryRequest, +) from ..base.api import ( ShedApiTestCase, skip_if_api_v1, @@ -49,6 +52,17 @@ def test_update_repository(self): ) assert repository_update.is_ok + def test_update_repository_info(self): + populator = self.populator + prefix = "testupdateinfo" + category_id = populator.new_category(prefix=prefix).id + repository = populator.new_repository(category_id, prefix=prefix) + repository_id = repository.id + request = UpdateRepositoryRequest(homepage_url="https://www.google.com") + update = populator.update(repository, request) + assert update.homepage_url == "https://www.google.com" + assert populator.get_repository(repository).homepage_url == "https://www.google.com" + # used by getRepository in TS client. def test_metadata_simple(self): populator = self.populator diff --git a/lib/tool_shed/util/repository_util.py b/lib/tool_shed/util/repository_util.py index 32829078f661..a5d4b90dd902 100644 --- a/lib/tool_shed/util/repository_util.py +++ b/lib/tool_shed/util/repository_util.py @@ -432,17 +432,26 @@ def change_repository_name_in_hgrc_file(hgrc_file: str, new_name: str) -> None: def update_repository(trans: "ProvidesUserContext", id: str, **kwds) -> Tuple[Optional["Repository"], Optional[str]]: """Update an existing ToolShed repository""" app = trans.app - message = None - flush_needed = False - sa_session = app.model.session repository = sa_session.get(app.model.Repository, app.security.decode_id(id)) if repository is None: return None, "Unknown repository ID" - if not (trans.user_is_admin or trans.app.security_agent.user_can_administer_repository(trans.user, repository)): + if not (trans.user_is_admin or app.security_agent.user_can_administer_repository(trans.user, repository)): message = "You are not the owner of this repository, so you cannot administer it." return None, message + return update_validated_repository(trans, repository, **kwds) + + +def update_validated_repository( + trans: "ProvidesUserContext", repository: "Repository", **kwds +) -> Tuple[Optional["Repository"], Optional[str]]: + """Update an existing ToolShed repository metadata once permissions have been checked.""" + app = trans.app + sa_session = app.model.session + message = None + flush_needed = False + # Allowlist properties that can be changed via this method for key in ("type", "description", "long_description", "remote_repository_url", "homepage_url"): # If that key is available, not None and different than what's in the model diff --git a/lib/tool_shed/webapp/api2/repositories.py b/lib/tool_shed/webapp/api2/repositories.py index ef510c088888..982840b22469 100644 --- a/lib/tool_shed/webapp/api2/repositories.py +++ b/lib/tool_shed/webapp/api2/repositories.py @@ -42,7 +42,10 @@ upload_tar_and_set_metadata, ) from tool_shed.structured_app import ToolShedApp -from tool_shed.util.repository_util import get_repository_in_tool_shed +from tool_shed.util.repository_util import ( + get_repository_in_tool_shed, + update_validated_repository, +) from tool_shed_client.schema import ( CreateRepositoryRequest, DetailedRepository, @@ -57,6 +60,7 @@ RepositoryUpdateRequest, ResetMetadataOnRepositoryRequest, ResetMetadataOnRepositoryResponse, + UpdateRepositoryRequest, ValidRepostiroyUpdateMessage, ) from . import ( @@ -293,6 +297,27 @@ def show( repository = get_repository_in_tool_shed(self.app, encoded_repository_id) return to_detailed_model(self.app, repository) + @router.put( + "/api/repositories/{encoded_repository_id}", + operation_id="repositories__update_repository", + ) + def update_repository( + self, + encoded_repository_id: str = RepositoryIdPathParam, + request: UpdateRepositoryRequest = Body(...), + ) -> DetailedRepository: + repository = get_repository_in_tool_shed(self.app, encoded_repository_id) + _ensure_can_manage(trans, repository) + + # may want to set some of these to null, so we're using the exclude_unset feature + # to just serialize the ones we want to use to a dictionary. + update_dictionary = request.model_dump(exclude_unset=True) + repo_result, message = update_validated_repository(trans, repository, **update_dictionary) + if repo_result is None: + raise ActionInputError(message) + + return to_detailed_model(self.app, repository) + @router.get( "/api/repositories/{encoded_repository_id}/permissions", operation_id="repositories__permissions", @@ -323,8 +348,7 @@ def show_allow_push( encoded_repository_id: str = RepositoryIdPathParam, ) -> List[str]: repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") + _ensure_can_manage(trans, repository) return trans.app.security_agent.usernames_that_can_push(repository) @router.post( @@ -390,8 +414,7 @@ def set_deprecated( encoded_repository_id: str = RepositoryIdPathParam, ): repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") + _ensure_can_manage(trans, repository) repository.deprecated = True trans.sa_session.add(repository) with transaction(trans.sa_session): @@ -409,8 +432,7 @@ def unset_deprecated( encoded_repository_id: str = RepositoryIdPathParam, ): repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") + _ensure_can_manage(trans, repository) repository.deprecated = False trans.sa_session.add(repository) with transaction(trans.sa_session): @@ -505,3 +527,8 @@ def get_readmes( ) -> dict: repository = get_repository_in_tool_shed(self.app, encoded_repository_id) return readmes(self.app, repository, changeset_revision) + + +def _ensure_can_manage(trans: SessionRequestContext, repository: Repository) -> None: + if not can_manage_repo(trans, repository): + raise InsufficientPermissionsException("You do not have permission to update this repository.") diff --git a/lib/tool_shed_client/schema/__init__.py b/lib/tool_shed_client/schema/__init__.py index 42c890ad4afd..bf9e9d93fc8a 100644 --- a/lib/tool_shed_client/schema/__init__.py +++ b/lib/tool_shed_client/schema/__init__.py @@ -123,6 +123,25 @@ class CreateRepositoryRequest(BaseModel): model_config = ConfigDict(populate_by_name=True) +class UpdateRepositoryRequest(BaseModel): + name: Optional[str] = None + synopsis: Optional[str] = None + type_: Optional[RepositoryType] = Field( + None, + alias="type", + title="Type", + ) + description: Optional[str] = None + remote_repository_url: Optional[str] = None + homepage_url: Optional[str] = None + category_ids: Optional[Union[List[str], str]] = Field( + None, + alias="category_ids[]", + title="Category IDs", + ) + model_config = ConfigDict(populate_by_name=True) + + class RepositoryUpdateRequest(BaseModel): commit_message: Optional[str] = None