Skip to content
33 changes: 33 additions & 0 deletions alembic/versions/00dab0f5f498_add_external_links_property_to_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add external links property to experiments

Revision ID: 00dab0f5f498
Revises: b22b450d409c
Create Date: 2025-12-16 12:06:15.265947

"""

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision = "00dab0f5f498"
down_revision = "b22b450d409c"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"experiments",
sa.Column("external_links", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("experiments", "external_links")
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions src/mavedb/models/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class Experiment(Base):
abstract_text = Column(String, nullable=False)
method_text = Column(String, nullable=False)
extra_metadata = Column(JSONB, nullable=False)
external_links = Column(JSONB, nullable=False, default={})

private = Column(Boolean, nullable=False, default=True)
approved = Column(Boolean, nullable=False, default=False)
Expand Down
17 changes: 13 additions & 4 deletions src/mavedb/routers/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ async def update_experiment(
item.raw_read_identifiers = raw_read_identifiers

if item_update.keywords:
keywords: list[ExperimentControlledKeywordAssociation] = []
all_labels_none = all(k.keyword.label is None for k in item_update.keywords)
if all_labels_none is False:
# Users may choose part of keywords from dropdown menu. Remove not chosen keywords from the list.
Expand All @@ -467,10 +468,18 @@ async def update_experiment(
validate_keyword_list(filtered_keywords)
except ValidationError as e:
raise HTTPException(status_code=422, detail=str(e))
try:
await item.set_keywords(db, filtered_keywords)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Invalid keywords: {str(e)}")
for upload_keyword in filtered_keywords:
try:
description = upload_keyword.description
controlled_keyword = search_keyword(db, upload_keyword.keyword.key, upload_keyword.keyword.label)
experiment_controlled_keyword = ExperimentControlledKeywordAssociation(
controlled_keyword=controlled_keyword,
description=description,
)
keywords.append(experiment_controlled_keyword)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
item.keyword_objs = keywords

item.modified_by = user_data.user

Expand Down
13 changes: 12 additions & 1 deletion src/mavedb/view_models/collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import date
from typing import Any, Sequence, Optional
from typing import Any, Optional, Sequence

from pydantic import Field, model_validator

Expand Down Expand Up @@ -132,3 +132,14 @@ class Collection(SavedCollection):
# NOTE: Coupled to ContributionRole enum
class AdminCollection(Collection):
pass


# Properties to return for official collections
class OfficialCollection(BaseModel):
badge_name: str
name: str
urn: str

class Config:
arbitrary_types_allowed = True
from_attributes = True
15 changes: 15 additions & 0 deletions src/mavedb/view_models/components/external_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Optional

from mavedb.view_models.base.base import BaseModel


class ExternalLink(BaseModel):
"""
Represents an external hyperlink for view models.

Attributes:
url (Optional[str]): Fully qualified URL for the external resource.
May be None if no link is available or applicable.
"""

url: Optional[str] = None
19 changes: 6 additions & 13 deletions src/mavedb/view_models/experiment.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
from datetime import date
from typing import Any, Collection, Optional, Sequence

from pydantic import field_validator, model_validator, ValidationInfo
from pydantic import ValidationInfo, field_validator, model_validator

from mavedb.lib.validation import urn_re
from mavedb.lib.validation.exceptions import ValidationError
from mavedb.lib.validation.transform import (
transform_experiment_set_to_urn,
transform_score_set_list_to_urn_list,
transform_record_publication_identifiers,
transform_score_set_list_to_urn_list,
)
from mavedb.lib.validation import urn_re
from mavedb.lib.validation.utilities import is_null
from mavedb.view_models import record_type_validator, set_record_type
from mavedb.view_models.base.base import BaseModel
from mavedb.view_models.collection import OfficialCollection
from mavedb.view_models.components.external_link import ExternalLink
from mavedb.view_models.contributor import Contributor, ContributorCreate
from mavedb.view_models.doi_identifier import (
DoiIdentifier,
Expand All @@ -37,16 +39,6 @@
from mavedb.view_models.user import SavedUser, User


class OfficialCollection(BaseModel):
badge_name: str
name: str
urn: str

class Config:
arbitrary_types_allowed = True
from_attributes = True


class ExperimentBase(BaseModel):
title: str
short_description: str
Expand Down Expand Up @@ -115,6 +107,7 @@ class SavedExperiment(ExperimentBase):
contributors: list[Contributor]
keywords: Sequence[SavedExperimentControlledKeyword]
score_set_urns: list[str]
external_links: dict[str, ExternalLink]

_record_type_factory = record_type_validator()(set_record_type)

Expand Down
22 changes: 5 additions & 17 deletions src/mavedb/view_models/score_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from mavedb.models.enums.processing_state import ProcessingState
from mavedb.view_models import record_type_validator, set_record_type
from mavedb.view_models.base.base import BaseModel
from mavedb.view_models.collection import OfficialCollection
from mavedb.view_models.components.external_link import ExternalLink
from mavedb.view_models.contributor import Contributor, ContributorCreate
from mavedb.view_models.doi_identifier import (
DoiIdentifier,
Expand Down Expand Up @@ -49,20 +51,6 @@
UnboundedRange = tuple[Union[float, None], Union[float, None]]


class ExternalLink(BaseModel):
url: Optional[str] = None


class OfficialCollection(BaseModel):
badge_name: str
name: str
urn: str

class Config:
arbitrary_types_allowed = True
from_attributes = True


class ScoreSetBase(BaseModel):
"""Base class for score set view models."""

Expand Down Expand Up @@ -109,7 +97,7 @@ def targets_need_labels_when_multiple_targets_exist(self) -> Self:
"Target sequence labels cannot be empty when multiple targets are defined.",
custom_loc=[
"body",
"targetGene",
"targetGenes",
idx,
"targetSequence",
"label",
Expand All @@ -134,7 +122,7 @@ def target_labels_are_unique(self) -> Self:
"Target sequence labels cannot be duplicated.",
custom_loc=[
"body",
"targetGene",
"targetGenes",
dup_indices[-1],
"targetSequence",
"label",
Expand All @@ -161,7 +149,7 @@ def target_accession_base_editor_targets_are_consistent(cls, field_value, values
"All target accessions must be of the same base editor type.",
custom_loc=[
"body",
"targetGene",
"targetGenes",
0,
"targetAccession",
"isBaseEditor",
Expand Down
65 changes: 60 additions & 5 deletions tests/helpers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@
"special": False,
"description": "Description",
},
{"key": "Delivery method", "label": "Other", "special": False, "description": "Description"},
{"key": "Delivery Method", "label": "Other", "special": False, "description": "Description"},
{
"key": "Phenotypic Assay Mechanism",
"label": "Other",
Expand All @@ -442,6 +442,13 @@
"special": False,
"description": "Description",
},
{
"key": "Phenotypic Assay Profiling Strategy",
"label": "Shotgun sequencing",
"code": None,
"special": False,
"description": "Description",
},
]

TEST_KEYWORDS = [
Expand Down Expand Up @@ -470,7 +477,7 @@
},
},
{
"keyword": {"key": "Delivery method", "label": "Other", "special": False, "description": "Description"},
"keyword": {"key": "Delivery Method", "label": "Other", "special": False, "description": "Description"},
"description": "Details of delivery method",
},
]
Expand All @@ -492,7 +499,7 @@
"methodText": "Methods",
"keywords": [
{
"keyword": {"key": "Delivery method", "label": "Other", "special": False, "description": "Description"},
"keyword": {"key": "Delivery Method", "label": "Other", "special": False, "description": "Description"},
"description": "Details of delivery method",
},
],
Expand Down Expand Up @@ -540,6 +547,7 @@
"primaryPublicationIdentifiers": [],
"secondaryPublicationIdentifiers": [],
"rawReadIdentifiers": [],
"externalLinks": {},
# keys to be set after receiving response
"urn": None,
"experimentSetUrn": None,
Expand Down Expand Up @@ -572,14 +580,60 @@
"keywords": [
{
"recordType": "ExperimentControlledKeyword",
"keyword": {"key": "Delivery method", "label": "Other", "special": False, "description": "Description"},
"keyword": {"key": "Delivery Method", "label": "Other", "special": False, "description": "Description"},
"description": "Details of delivery method",
},
],
"doiIdentifiers": [],
"primaryPublicationIdentifiers": [],
"secondaryPublicationIdentifiers": [],
"rawReadIdentifiers": [],
"externalLinks": {},
# keys to be set after receiving response
"urn": None,
"experimentSetUrn": None,
"officialCollections": [],
"numScoreSets": 0, # NOTE: This is context-dependent and may need overriding per test
}

TEST_EXPERIMENT_WITH_UPDATE_KEYWORD_RESPONSE = {
"recordType": "Experiment",
"title": "Test Experiment Title",
"shortDescription": "Test experiment",
"abstractText": "Abstract",
"methodText": "Methods",
"createdBy": {
"recordType": "User",
"firstName": TEST_USER["first_name"],
"lastName": TEST_USER["last_name"],
"orcidId": TEST_USER["username"],
},
"modifiedBy": {
"recordType": "User",
"firstName": TEST_USER["first_name"],
"lastName": TEST_USER["last_name"],
"orcidId": TEST_USER["username"],
},
"creationDate": date.today().isoformat(),
"modificationDate": date.today().isoformat(),
"scoreSetUrns": [],
"contributors": [],
"keywords": [
{
"recordType": "ExperimentControlledKeyword",
"keyword": {
"key": "Phenotypic Assay Profiling Strategy",
"label": "Shotgun sequencing",
"special": False,
"description": "Description"
},
"description": "Details of phenotypic assay profiling strategy",
},
],
"doiIdentifiers": [],
"primaryPublicationIdentifiers": [],
"secondaryPublicationIdentifiers": [],
"rawReadIdentifiers": [],
# keys to be set after receiving response
"urn": None,
"experimentSetUrn": None,
Expand Down Expand Up @@ -622,14 +676,15 @@
},
{
"recordType": "ExperimentControlledKeyword",
"keyword": {"key": "Delivery method", "label": "Other", "special": False, "description": "Description"},
"keyword": {"key": "Delivery Method", "label": "Other", "special": False, "description": "Description"},
"description": "Description",
},
],
"doiIdentifiers": [],
"primaryPublicationIdentifiers": [],
"secondaryPublicationIdentifiers": [],
"rawReadIdentifiers": [],
"externalLinks": {},
# keys to be set after receiving response
"urn": None,
"experimentSetUrn": None,
Expand Down
Loading
Loading