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

feat: data submitters want collections to document their consortia #3875

Merged
merged 4 commits into from
Jan 12, 2023
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
16 changes: 13 additions & 3 deletions backend/layers/api/portal_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
CollectionVersionException,
DatasetInWrongStatusException,
DatasetNotFoundException,
InvalidMetadataException,
InvalidURIException,
MaxFileSizeExceededException,
)
Expand Down Expand Up @@ -217,7 +218,7 @@ def _collection_to_response(collection: CollectionVersionWithDatasets, access_ty
is_tombstoned = collection.canonical_collection.tombstoned
is_in_published_collection = collection.published_at is not None

return remove_none(
response = remove_none(
{
"access_type": access_type,
"contact_email": collection.metadata.contact_email,
Expand All @@ -243,6 +244,10 @@ def _collection_to_response(collection: CollectionVersionWithDatasets, access_ty
}
)

# Always return consortia
response["consortia"] = collection.metadata.consortia
return response


def lookup_collection(collection_id: str):
"""
Expand Down Expand Up @@ -337,11 +342,12 @@ def create_collection(body: dict, user: str):
body["contact_name"],
body["contact_email"],
[_link_from_request(node) for node in body.get("links", [])],
body.get("consortia", []),
)

try:
version = get_business_logic().create_collection(user, curator_name, metadata)
except CollectionCreationException as ex:
except (InvalidMetadataException, CollectionCreationException) as ex:
raise InvalidParametersHTTPException(detail=ex.errors)

return make_response(jsonify({"collection_id": version.collection_id.id}), 201)
Expand Down Expand Up @@ -424,9 +430,13 @@ def update_collection(collection_id: str, body: dict, token_info: dict):
body.get("contact_name"),
body.get("contact_email"),
update_links,
body.get("consortia"),
)

get_business_logic().update_collection_version(version.version_id, payload)
try:
get_business_logic().update_collection_version(version.version_id, payload)
except InvalidMetadataException as ex:
raise InvalidParametersHTTPException(detail=ex.errors)

# Requires strong consistency w.r.t. the operation above - if not available, the update needs
# to be done in memory
Expand Down
5 changes: 4 additions & 1 deletion backend/layers/business/business.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def create_collection(
"""

errors = []
# Check metadata is valid
collection_metadata.strip_fields()
validation.verify_collection_metadata(collection_metadata, errors)

# TODO: Maybe switch link.type to be an enum
Expand All @@ -115,7 +117,6 @@ def create_collection(
if errors:
raise CollectionCreationException(errors)

collection_metadata.strip_fields()
created_version = self.database_provider.create_canonical_collection(owner, curator_name, collection_metadata)

# TODO: can collapse with `create_canonical_collection`
Expand Down Expand Up @@ -210,6 +211,8 @@ def update_collection_version(self, version_id: CollectionVersionId, body: Colle
# TODO: link.type should DEFINITELY move to an enum. pylance will help with the refactor

errors = []

# Check metadata
validation.verify_collection_metadata_update(body, errors)

current_version = self.get_collection_version(version_id)
Expand Down
1 change: 1 addition & 0 deletions backend/layers/business/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ class CollectionMetadataUpdate:
contact_name: Optional[str]
contact_email: Optional[str]
links: Optional[List[Link]]
consortia: Optional[List[str]]
6 changes: 6 additions & 0 deletions backend/layers/business/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def __init__(self, errors: Optional[List[str]] = None) -> None:
super().__init__()


class InvalidMetadataException(BusinessException):
def __init__(self, errors: Optional[List[str]] = None) -> None:
self.errors: Optional[List[str]] = errors
super().__init__()


class CollectionVersionException(BusinessException):
pass

Expand Down
5 changes: 4 additions & 1 deletion backend/layers/common/entities.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import List, Optional
Expand Down Expand Up @@ -211,6 +211,7 @@ class CollectionMetadata:
contact_name: str
contact_email: str
links: List[Link]
consortia: List[str] = field(default_factory=list)

def strip_fields(self):
self.name = self.name.strip()
Expand All @@ -219,6 +220,8 @@ def strip_fields(self):
self.contact_email = self.contact_email.strip()
for link in self.links:
link.strip_fields()
if self.consortia is not None:
self.consortia = [consortium.strip() for consortium in self.consortia]


@dataclass
Expand Down
32 changes: 32 additions & 0 deletions backend/layers/common/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,31 @@

from urllib.parse import urlparse
from backend.layers.business.entities import CollectionMetadataUpdate
from backend.layers.business.exceptions import InvalidMetadataException

from backend.layers.common.entities import CollectionMetadata, Link
from backend.layers.common.regex import CONTROL_CHARS, EMAIL_REGEX

control_char_re = re.compile(CONTROL_CHARS)

valid_consortia = {
"Allen Institute for Brain Science",
"BRAIN Initiative",
"CZ Biohub",
"CZI Neurodegeneration Challenge Network",
"CZI Single-Cell Biology",
"European Union’s Horizon 2020",
"GenitoUrinary Development Molecular Anatomy Project (GUDMAP)",
"Gut Cell Atlas",
"Human BioMolecular Atlas Program (HuBMAP)",
"Human Pancreas Analysis Program (HPAP)",
"Human Tumor Atlas Network (HTAN)",
"Kidney Precision Medicine Project (KPMP)",
"LungMAP",
"SEA-AD",
"Wellcome HCA Strategic Science Support",
}


def _verify_collection_metadata_fields(
metadata: Union[CollectionMetadata, CollectionMetadataUpdate], check_existence: bool, errors: list
Expand All @@ -33,6 +52,13 @@ def check(key):
else:
return value

def verify_collection_consortia(metadata: Union[CollectionMetadata, CollectionMetadataUpdate], errors: list):
consortia = getattr(metadata, "consortia")
if consortia:
for consortium in consortia:
if consortium not in valid_consortia:
errors.append({"name": "consortia", "reason": "Invalid consortia."})

contact_email = check("contact_email")
if contact_email:
result = EMAIL_REGEX.match(contact_email)
Expand All @@ -43,6 +69,8 @@ def check(key):
check("name")
check("contact_name")

verify_collection_consortia(metadata, errors)


def verify_collection_links(links: List[Link], errors: list) -> None:
def _error_message(i: int, _url: str) -> dict:
Expand All @@ -60,6 +88,8 @@ def _error_message(i: int, _url: str) -> dict:

def verify_collection_metadata_update(metadata: CollectionMetadataUpdate, errors: list) -> None:
_verify_collection_metadata_fields(metadata, check_existence=False, errors=errors)
if errors:
raise InvalidMetadataException(errors=errors)
if metadata.links is not None:
verify_collection_links(metadata.links, errors)

Expand All @@ -69,4 +99,6 @@ def verify_collection_metadata(metadata: CollectionMetadata, errors: list) -> No
Verify if `CollectionMetadata` is well defined. Since `CollectionMetadataCreate
"""
_verify_collection_metadata_fields(metadata, check_existence=True, errors=errors)
if errors:
raise InvalidMetadataException(errors=errors)
verify_collection_links(metadata.links, errors)
14 changes: 14 additions & 0 deletions backend/portal/api/app/portal-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ paths:
description: name of the curator of the collection.
links:
$ref: "#/components/schemas/links"
consortia:
$ref: "#/components/schemas/consortia"
responses:
"201":
description: A new collection has been created.
Expand Down Expand Up @@ -208,6 +210,8 @@ paths:
description: name of the curator of the collection
links:
$ref: "#/components/schemas/links"
consortia:
$ref: "#/components/schemas/consortia"
responses:
"200":
description: OK
Expand Down Expand Up @@ -445,6 +449,8 @@ paths:
type: number
revised_at:
type: number
consortia:
$ref: "#/components/schemas/consortia"
"400":
$ref: "#/components/responses/400"

Expand Down Expand Up @@ -977,6 +983,11 @@ components:
link_type:
type: string
enum: [PROTOCOL, RAW_DATA, DOI, LAB_WEBSITE, OTHER, DATA_SOURCE]
consortia:
type: array
items:
type: string
description: The broader research groups which contributed to this collection of datasets.
collection:
type: object
required:
Expand All @@ -986,6 +997,7 @@ components:
- visibility
- links
- datasets
- consortia
- created_at
- updated_at
- data_submission_policy_version
Expand All @@ -1007,6 +1019,8 @@ components:
$ref: "#/components/schemas/visibility"
links:
$ref: "#/components/schemas/links"
consortia:
$ref: "#/components/schemas/consortia"
contact_name:
type: string
contact_email:
Expand Down
11 changes: 11 additions & 0 deletions backend/portal/api/curation/curation-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,8 @@ components:
The CELLxGENE Discover URL for the Collection. This points to the canonical link unless it's a revision,
in which case it points to the unpublished revision version.
type: string
consortia:
$ref: "#/components/schemas/consortia"
contact_email:
$ref: "#/components/schemas/contact_email"
contact_name:
Expand Down Expand Up @@ -627,6 +629,11 @@ components:
collection_url:
description: The CELLxGENE Discover URL for the Collection.
type: string
consortia:
type: array
items:
type: string
description: The broader research groups which contributed to this collection of datasets.
contact_email:
description: The email of contact person for the Collection. Example email john.smith@email.com
type: string
Expand Down Expand Up @@ -681,6 +688,8 @@ components:
properties:
collection_url:
$ref: "#/components/schemas/collection_url"
consortia:
$ref: "#/components/schemas/consortia"
contact_email:
$ref: "#/components/schemas/contact_email"
contact_name:
Expand Down Expand Up @@ -721,6 +730,8 @@ components:
type: object
collection_form_metadata:
properties:
consortia:
$ref: "#/components/schemas/consortia"
contact_email:
$ref: "#/components/schemas/contact_email"
contact_name:
Expand Down
13 changes: 11 additions & 2 deletions backend/portal/api/curation/v1/curation/collections/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from backend.layers.api.providers import get_business_logic
from backend.layers.auth.user_info import UserInfo
from backend.layers.business.entities import CollectionQueryFilter
from backend.layers.business.exceptions import CollectionCreationException
from backend.layers.business.exceptions import CollectionCreationException, InvalidMetadataException
from backend.layers.common import doi
from backend.layers.common.entities import CollectionMetadata, Link
from backend.portal.api.curation.v1.curation.collections.common import reshape_for_curation_api
Expand Down Expand Up @@ -58,10 +58,19 @@ def post(body: dict, user: str):

# Build CollectionMetadata object
links = [Link(link.get("link_name"), link["link_type"], link["link_url"]) for link in body.get("links", [])]
metadata = CollectionMetadata(body["name"], body["description"], body["contact_name"], body["contact_email"], links)
metadata = CollectionMetadata(
body["name"],
body["description"],
body["contact_name"],
body["contact_email"],
links,
body.get("consortia", []),
)

try:
version = get_business_logic().create_collection(user, "", metadata)
except InvalidMetadataException as ex:
errors.extend(ex.errors)
except CollectionCreationException as ex:
errors.extend(ex.errors)
if errors:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from backend.layers.api.providers import get_business_logic
from backend.layers.auth.user_info import UserInfo
from backend.layers.business.entities import CollectionMetadataUpdate
from backend.layers.business.exceptions import CollectionUpdateException
from backend.layers.business.exceptions import CollectionUpdateException, InvalidMetadataException
from backend.layers.common import doi
from backend.layers.common.entities import Link
from backend.portal.api.curation.v1.curation.collections.common import (
Expand Down Expand Up @@ -81,11 +81,14 @@ def _link_from_request(body: dict):
body.get("contact_name"),
body.get("contact_email"),
update_links,
body.get("consortia", []),
)

# Update the collection
try:
get_business_logic().update_collection_version(collection_version.version_id, collection_metadata)
except InvalidMetadataException as ex:
raise InvalidParametersHTTPException(ext=dict(invalid_parameters=ex.errors))
except CollectionUpdateException as ex:
errors.extend(ex.errors)
if errors:
Expand Down
2 changes: 2 additions & 0 deletions backend/portal/api/curation/v1/curation/collections/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def reshape_for_curation_api(
revised_at = None
response = dict(
collection_url=collection_url,
consortia=collection_version.metadata.consortia,
contact_email=collection_version.metadata.contact_email,
contact_name=collection_version.metadata.contact_name,
created_at=collection_version.created_at,
Expand Down Expand Up @@ -215,6 +216,7 @@ class EntityColumns:
"links",
"datasets",
"revising_in",
"consortia",
]

link_cols = [
Expand Down
1 change: 1 addition & 0 deletions frontend/src/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface Author {

export interface Collection {
access_type: ACCESS_TYPE;
consortia: string[];
NoopDog marked this conversation as resolved.
Show resolved Hide resolved
contact_email: string;
contact_name: string;
description: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
export enum CONSORTIA {
ALLEN_INSTITUTE_FOR_BRAIN_SCIENCE = "Allen Institute for Brain Science",
BRAIN_INITIATIVE = "BRAIN Initiative",
CZ_BIOHUB = "CZ Biohub",
CZI_NDCN = "CZI Neurodegeneration Challenge Network",
CZI_SCB = "CZI Single-Cell Biology",
EU_HORIZON_2020 = "European Union’s Horizon 2020",
GUDMAP = "GenitoUrinary Development Molecular Anatomy Project (GUDMAP)",
GUT_CELL_ATLAS = "Gut Cell Atlas",
HUBMAP = "Human BioMolecular Atlas Program (HuBMAP)",
HPAP = "Human Pancreas Analysis Program (HPAP)",
HTAN = "Human Tumor Atlas Network (HTAN)",
KPMP = "Kidney Precision Medicine Project (KPMP)",
LUNG_MAP = "LungMAP",
SEA_AD = "SEA-AD",
WELCOME_HCA_STRATEGIC_SCIENCE_SUPPORT = "Wellcome HCA Strategic Science Support",
}

export const DEBOUNCE_TIME_MS = 100;

/**
Expand Down
Loading