-
Notifications
You must be signed in to change notification settings - Fork 14
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
feat: add canonical collection tombstoning to portal redesign #3740
Changes from 2 commits
01dfbce
deb7a4f
2f87a36
f992333
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
|
||
from backend.common.utils.http_exceptions import ( | ||
ConflictException, | ||
GoneHTTPException, | ||
ForbiddenHTTPException, | ||
InvalidParametersHTTPException, | ||
MethodNotAllowedException, | ||
|
@@ -141,7 +142,7 @@ def remove_none(self, body: dict): | |
return {k: v for k, v in body.items() if v is not None} | ||
|
||
# Note: `metadata` can be none while the dataset is uploading | ||
def _dataset_to_response(self, dataset: DatasetVersion): | ||
def _dataset_to_response(self, dataset: DatasetVersion, is_tombstoned): | ||
return self.remove_none( | ||
{ | ||
"assay": None | ||
|
@@ -186,7 +187,7 @@ def _dataset_to_response(self, dataset: DatasetVersion): | |
"tissue": None | ||
if dataset.metadata is None | ||
else self._ontology_term_ids_to_response(dataset.metadata.tissue), | ||
"tombstone": False, # TODO | ||
"tombstone": is_tombstoned, | ||
"updated_at": dataset.created_at, # Legacy: datasets can't be updated anymore | ||
"x_approximate_distribution": None | ||
if dataset.metadata is None | ||
|
@@ -196,6 +197,7 @@ def _dataset_to_response(self, dataset: DatasetVersion): | |
|
||
def _collection_to_response(self, collection: CollectionVersion, access_type: str): | ||
collection_id = collection.collection_id.id if collection.published_at is not None else collection.version_id.id | ||
is_tombstoned = collection.canonical_collection.tombstoned | ||
return self.remove_none( | ||
{ | ||
"access_type": access_type, | ||
|
@@ -204,7 +206,7 @@ def _collection_to_response(self, collection: CollectionVersion, access_type: st | |
"created_at": collection.created_at, | ||
"curator_name": "", # TODO | ||
"data_submission_policy_version": "1.0", # TODO | ||
"datasets": [self._dataset_to_response(ds) for ds in collection.datasets], | ||
"datasets": [self._dataset_to_response(ds, is_tombstoned=is_tombstoned) for ds in collection.datasets], | ||
"description": collection.metadata.description, | ||
"id": collection_id, | ||
"links": [self._link_to_response(link) for link in collection.metadata.links], | ||
|
@@ -225,13 +227,16 @@ def get_collection_details(self, collection_id: str, token_info: dict): | |
version = self.business_logic.get_published_collection_version(CollectionId(collection_id)) | ||
if version is None: | ||
version = self.business_logic.get_collection_version(CollectionVersionId(collection_id)) | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: else is probably not needed here |
||
if version.canonical_collection.tombstoned: | ||
raise GoneHTTPException() | ||
|
||
if version is None: | ||
raise ForbiddenHTTPException() # TODO: maybe remake this exception | ||
|
||
# TODO: handle tombstoning | ||
|
||
|
||
user_info = UserInfo(token_info) | ||
print(token_info, user_info.user_id, version.owner) | ||
access_type = "WRITE" if user_info.is_user_owner_or_allowed(version.owner) else "READ" | ||
|
||
response = self._collection_to_response(version, access_type) | ||
|
@@ -329,15 +334,25 @@ def get_collection_index(self): | |
|
||
def delete_collection(self, collection_id: str, token_info: dict): | ||
""" | ||
Deletes a collection version from the persistence store. | ||
Deletes a collection version from the persistence store, or tombstones a canonical collection. | ||
""" | ||
version = self.business_logic.get_collection_version(CollectionVersionId(collection_id)) | ||
resource_id = CollectionVersionId(collection_id) | ||
version = self.business_logic.get_collection_version(resource_id) | ||
if version is None: | ||
raise ForbiddenHTTPException() | ||
resource_id = CollectionId(collection_id) | ||
version = self.business_logic.get_collection_version_from_canonical(resource_id) | ||
if version is None: | ||
raise ForbiddenHTTPException() | ||
if not UserInfo(token_info).is_user_owner_or_allowed(version.owner): | ||
raise ForbiddenHTTPException() | ||
|
||
self.business_logic.delete_collection_version(CollectionVersionId(collection_id)) | ||
if isinstance(resource_id, CollectionVersionId): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure about using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can replace with a flag if you prefer. Or we can rework the underlying business logic method to fetch the collection/collection version before deleting and handle the logic there. The only issue is we'd be introducing a 2nd set of lookups, since we need the lookup here to get the version owner for the auth logic handling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's leave it as is |
||
try: | ||
self.business_logic.delete_collection_version(resource_id) | ||
except CollectionIsPublishedException: | ||
raise ForbiddenHTTPException() | ||
elif isinstance(resource_id, CollectionId): | ||
self.business_logic.tombstone_collection(resource_id) | ||
|
||
def update_collection(self, collection_id: str, body: dict, token_info: dict): | ||
""" | ||
|
@@ -523,7 +538,7 @@ def get_datasets_index(self): | |
|
||
response = [] | ||
for dataset in self.business_logic.get_all_published_datasets(): | ||
payload = self._dataset_to_response(dataset) | ||
payload = self._dataset_to_response(dataset, is_tombstoned=False) | ||
enrich_dataset_with_ancestors( | ||
payload, "development_stage", ontology_mappings.development_stage_ontology_mapping | ||
) | ||
|
@@ -555,7 +570,7 @@ def get_dataset_identifiers(self, url: str): | |
""" | ||
try: | ||
path = urlparse(url).path | ||
id = [segment for segment in path.split("/") if segment][-1].strip(".cxg") | ||
id = [segment for segment in path.split("/") if segment][-1].removesuffix(".cxg") | ||
except Exception: | ||
raise ServerErrorHTTPException("Cannot parse URL") | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,9 +47,9 @@ | |
DatasetValidationStatus, | ||
DatasetVersion, | ||
DatasetVersionId, | ||
Link | ||
Link, | ||
) | ||
from backend.layers.persistence.persistence_interface import DatabaseProviderInterface | ||
from backend.layers.persistence.persistence_interface import DatabaseProviderInterface, PersistenceException | ||
from backend.layers.thirdparty.crossref_provider import CrossrefProviderInterface | ||
from backend.layers.thirdparty.s3_provider import S3Provider | ||
from backend.layers.thirdparty.step_function_provider import StepFunctionProviderInterface | ||
|
@@ -154,6 +154,8 @@ def get_collection_version_from_canonical( | |
return published_version | ||
else: | ||
all_versions = self.get_collection_versions_from_canonical(collection_id) | ||
if not all_versions: | ||
return None | ||
return next(v for v in all_versions if v.published_at is None) | ||
|
||
def get_collections(self, filter: CollectionQueryFilter) -> Iterable[CollectionVersion]: | ||
|
@@ -423,9 +425,8 @@ def create_collection_version(self, collection_id: CollectionId) -> CollectionVe | |
Also ensures that the collection does not have any active, unpublished version | ||
""" | ||
|
||
try: | ||
all_versions = self.database_provider.get_all_versions_for_collection(collection_id) | ||
except Exception: # TODO: maybe add a RecordNotFound exception for finer grained exceptions | ||
all_versions = self.database_provider.get_all_versions_for_collection(collection_id) | ||
if not all_versions: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
raise CollectionVersionException(f"Collection {collection_id} cannot be found") | ||
|
||
if any(v for v in all_versions if v.published_at is None): | ||
|
@@ -437,6 +438,9 @@ def create_collection_version(self, collection_id: CollectionId) -> CollectionVe | |
def delete_collection_version(self, version_id: CollectionVersionId) -> None: | ||
self.database_provider.delete_collection_version(version_id) | ||
|
||
def tombstone_collection(self, collection_id: CollectionId) -> None: | ||
self.database_provider.delete_canonical_collection(collection_id) | ||
|
||
def publish_collection_version(self, version_id: CollectionVersionId) -> None: | ||
version = self.database_provider.get_collection_version(version_id) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,8 @@ | |
DatasetVersion, | ||
DatasetVersionId, | ||
) | ||
from backend.layers.persistence.persistence import DatabaseProviderInterface | ||
from backend.layers.persistence.persistence import DatabaseProviderInterface, PersistenceException | ||
from backend.layers.business.exceptions import CollectionIsPublishedException | ||
|
||
|
||
class DatabaseProviderMock(DatabaseProviderInterface): | ||
|
@@ -82,8 +83,9 @@ def create_canonical_collection(self, owner: str, collection_metadata: Collectio | |
# Don't set mappings here - those will be set when publishing the collection! | ||
return copy.deepcopy(version) | ||
|
||
def _update_version_with_canonical(self, version: Union[CollectionVersion, CollectionVersionWithDatasets], | ||
update_datasets: bool = False): | ||
def _update_version_with_canonical( | ||
self, version: Union[CollectionVersion, CollectionVersionWithDatasets], update_datasets: bool = False | ||
): | ||
""" | ||
Private method that returns a version updated with the canonical collection. | ||
This is equivalent to a database double lookup (or join). | ||
|
@@ -119,10 +121,14 @@ def get_all_collections_versions(self) -> Iterable[CollectionVersion]: # TODO: | |
def get_all_mapped_collection_versions(self) -> Iterable[CollectionVersion]: # TODO: add filters if needed | ||
for version_id, collection_version in self.collections_versions.items(): | ||
if version_id in [c.version_id.id for c in self.collections.values()]: | ||
collection_id = collection_version.collection_id.id | ||
if self.collections[collection_id].tombstoned: | ||
continue | ||
yield self._update_version_with_canonical(collection_version) | ||
|
||
def delete_collection(self, collection_id: CollectionId) -> None: | ||
del self.collections[collection_id.id] | ||
def delete_canonical_collection(self, collection_id: CollectionId) -> None: | ||
collection = self.collections[collection_id.id] | ||
collection.tombstoned = True | ||
|
||
def save_collection_metadata( | ||
self, version_id: CollectionVersionId, collection_metadata: CollectionMetadata | ||
|
@@ -159,7 +165,9 @@ def add_collection_version(self, collection_id: CollectionId) -> CollectionVersi | |
return new_version_id | ||
|
||
def delete_collection_version(self, version_id: CollectionVersionId) -> None: | ||
# Can only delete an unpublished collection | ||
collection_version = self.collections_versions.get(version_id.id) | ||
if collection_version.published_at: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I generally prefer an explicit |
||
raise CollectionIsPublishedException("Can only delete unpublished collections") | ||
del self.collections_versions[version_id.id] | ||
|
||
def get_collection_version(self, version_id: CollectionVersionId) -> CollectionVersion: | ||
|
@@ -173,8 +181,6 @@ def get_all_versions_for_collection(self, collection_id: CollectionId) -> Iterab | |
for collection_version in self.collections_versions.values(): | ||
if collection_version.collection_id == collection_id: | ||
versions.append(self._update_version_with_canonical(collection_version, update_datasets=True)) | ||
if not versions: | ||
raise ValueError("Could not find matching collection Id") | ||
return versions | ||
|
||
def get_collection_version_with_datasets(self, version_id: CollectionVersionId) -> CollectionVersionWithDatasets: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.