Skip to content

Commit

Permalink
Restore repository update in the tool shed 2.0.
Browse files Browse the repository at this point in the history
I guess somehow this critical API endpoint had no tests.
  • Loading branch information
jmchilton committed Aug 8, 2024
1 parent 5f635a3 commit 1a7e5d1
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 19 deletions.
9 changes: 7 additions & 2 deletions lib/tool_shed/managers/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,7 @@ def get_repository_metadata_for_management(
trans: ProvidesUserContext, encoded_repository_id: str, changeset_revision: str
) -> RepositoryMetadata:
repository = get_repository_in_tool_shed(trans.app, encoded_repository_id)
if not can_manage_repo(trans, repository):
raise InsufficientPermissionsException("Cannot manage target repository")
ensure_can_manage(trans, repository, "Cannot manage target repository")
revisions = [r for r in repository.metadata_revisions if r.changeset_revision == changeset_revision]
if len(revisions) != 1:
raise ObjectNotFound()
Expand Down Expand Up @@ -582,6 +581,12 @@ def upload_tar_and_set_metadata(
return message


def ensure_can_manage(trans: ProvidesUserContext, repository: Repository, error_message: Optional[str] = None) -> None:
if not can_manage_repo(trans, repository):
error_message = error_message or "You do not have permission to update this repository."
raise InsufficientPermissionsException(error_message)


def _get_repository_by_name_and_owner(session: scoped_session, name: str, owner: str, user_model):
stmt = (
select(Repository)
Expand Down
20 changes: 18 additions & 2 deletions lib/tool_shed/test/base/populators.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Category,
CreateCategoryRequest,
CreateRepositoryRequest,
DetailedRepository,
from_legacy_install_info,
GetInstallInfoRequest,
GetOrderedInstallableRevisionsRequest,
Expand All @@ -41,6 +42,7 @@
ResetMetadataOnRepositoryResponse,
ToolSearchRequest,
ToolSearchResults,
UpdateRepositoryRequest,
Version,
)
from .api_util import (
Expand Down Expand Up @@ -209,6 +211,17 @@ def upload_revision_raw(
)
return response

def update_raw(self, repository: HasRepositoryId, request: UpdateRepositoryRequest) -> requests.Response:
repository_id = self._repository_id(repository)
body_json = request.model_dump(exclude_unset=True, by_alias=True)
put_response = self._api_interactor.put(f"repositories/{repository_id}", json=body_json)
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:
Expand Down Expand Up @@ -358,11 +371,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)
Expand Down
39 changes: 38 additions & 1 deletion lib/tool_shed/test/functional/test_shed_repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,6 +52,40 @@ 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_id, request)
assert update.homepage_url == "https://www.google.com"
assert populator.get_repository(repository_id).homepage_url == "https://www.google.com"

def test_update_category(self):
populator = self.populator
prefix = "testupdatecategory"

old_category_id = populator.new_category(prefix=prefix).id
new_category_id = populator.new_category(prefix=prefix).id

populator.assert_category_has_n_repositories(old_category_id, 0)
populator.assert_category_has_n_repositories(new_category_id, 0)

repository = populator.new_repository(old_category_id, prefix=prefix)
repository_id = repository.id

populator.assert_category_has_n_repositories(old_category_id, 1)
populator.assert_category_has_n_repositories(new_category_id, 0)

request = UpdateRepositoryRequest(category_ids=[new_category_id])
populator.update(repository_id, request)

# does this mean we cannot remove categories?
populator.assert_category_has_n_repositories(old_category_id, 0)
populator.assert_category_has_n_repositories(new_category_id, 1)

# used by getRepository in TS client.
def test_metadata_simple(self):
populator = self.populator
Expand Down
21 changes: 15 additions & 6 deletions lib/tool_shed/util/repository_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,17 +432,27 @@ 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
Expand All @@ -451,10 +461,9 @@ def update_repository(trans: "ProvidesUserContext", id: str, **kwds) -> Tuple[Op
flush_needed = True

if "category_ids" in kwds and isinstance(kwds["category_ids"], list):

# Remove existing category associations
delete_repository_category_associations(
sa_session, app.model.RepositoryCategoryAssociation, app.security.decode_id(id)
)
delete_repository_category_associations(sa_session, app.model.RepositoryCategoryAssociation, repository.id)

# Then (re)create category associations
for category_id in kwds["category_ids"]:
Expand Down
43 changes: 35 additions & 8 deletions lib/tool_shed/webapp/api2/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
)
from starlette.datastructures import UploadFile as StarletteUploadFile

from galaxy.exceptions import InsufficientPermissionsException
from galaxy.exceptions import (
ActionInputError,
InsufficientPermissionsException,
)
from galaxy.model.base import transaction
from galaxy.webapps.galaxy.api import as_form
from tool_shed.context import SessionRequestContext
Expand All @@ -28,6 +31,7 @@
can_update_repo,
check_updates,
create_repository,
ensure_can_manage,
get_install_info,
get_ordered_installable_revisions,
get_repository_metadata_dict,
Expand All @@ -42,7 +46,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,
Expand All @@ -57,6 +64,7 @@
RepositoryUpdateRequest,
ResetMetadataOnRepositoryRequest,
ResetMetadataOnRepositoryResponse,
UpdateRepositoryRequest,
ValidRepostiroyUpdateMessage,
)
from . import (
Expand Down Expand Up @@ -293,6 +301,28 @@ 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,
trans: SessionRequestContext = DependsOnTrans,
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",
Expand Down Expand Up @@ -323,8 +353,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(
Expand Down Expand Up @@ -390,8 +419,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):
Expand All @@ -409,8 +437,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):
Expand Down
53 changes: 53 additions & 0 deletions lib/tool_shed/webapp/frontend/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export interface paths {
"/api/repositories/{encoded_repository_id}": {
/** Show */
get: operations["repositories__show"]
/** Update Repository */
put: operations["repositories__update_repository"]
}
"/api/repositories/{encoded_repository_id}/allow_push": {
/** Show Allow Push */
Expand Down Expand Up @@ -2093,6 +2095,23 @@ export interface components {
/** Email */
email: string
}
/** UpdateRepositoryRequest */
UpdateRepositoryRequest: {
/** Category IDs */
category_ids?: string[] | null
/** Description */
description?: string | null
/** Homepage Url */
homepage_url?: string | null
/** Name */
name?: string | null
/** Remote Repository Url */
remote_repository_url?: string | null
/** Synopsis */
synopsis?: string | null
/** Type */
type?: ("repository_suite_definition" | "tool_dependency_definition" | "unrestricted") | null
}
/** UserV2 */
UserV2: {
/** Id */
Expand Down Expand Up @@ -2701,6 +2720,40 @@ export interface operations {
}
}
}
/** Update Repository */
repositories__update_repository: {
parameters: {
path: {
/** @description The encoded database identifier of the repository. */
encoded_repository_id: string
}
}
requestBody: {
content: {
"application/json": components["schemas"]["UpdateRepositoryRequest"]
}
}
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["DetailedRepository"]
}
}
/** @description Request Error */
"4XX": {
content: {
"application/json": components["schemas"]["MessageExceptionModel"]
}
}
/** @description Server Error */
"5XX": {
content: {
"application/json": components["schemas"]["MessageExceptionModel"]
}
}
}
}
/** Show Allow Push */
repositories__show_allow_push: {
parameters: {
Expand Down
19 changes: 19 additions & 0 deletions lib/tool_shed_client/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[List[str]] = Field(
None,
alias="category_ids",
title="Category IDs",
)
model_config = ConfigDict(populate_by_name=True)


class RepositoryUpdateRequest(BaseModel):
commit_message: Optional[str] = None

Expand Down

0 comments on commit 1a7e5d1

Please sign in to comment.