Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore repository update in the tool shed 2.0. #18655

Merged
merged 1 commit into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading