Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
6415ff0
Only show unpublished superseding score set to the users who have per…
EstelleDa Dec 5, 2024
7fc2add
Modify function and add superseding score set tests.
EstelleDa Dec 13, 2024
131e3b1
Remove unnecessary import.
EstelleDa Dec 13, 2024
882ae30
Fix listing score set in experiment if these score sets' superseding …
EstelleDa Dec 20, 2024
c7165f0
Handle poetry run mypy src/ error so that add next_version
EstelleDa Dec 20, 2024
bec2b5c
Convert the file format back.
EstelleDa Jan 13, 2025
5361fd7
Find a potential bug from the failed tests. Haven't add related new t…
EstelleDa Jan 22, 2025
cfb8c9d
DB Column for Score Set Level Score Thresholds
bencap Nov 13, 2024
7d09e35
Add Threshold Calibrations to Score Set Models and Tests
bencap Nov 15, 2024
854417b
Reorder Alembic Migrations for Release
bencap Nov 20, 2024
af5606f
Fix typo in calibration validation message
bencap Dec 31, 2024
63ee6e0
Modify multiple functions that relates to superseding score set and a…
EstelleDa Jan 27, 2025
e75f745
Collections Data Model
bencap Oct 15, 2024
4fd1a0d
DB Migration for Collections Data Model
bencap Oct 15, 2024
5cc9735
Move collection urn property to SavedCollection
sallybg Oct 15, 2024
639c463
Add method to create a collection
sallybg Oct 16, 2024
40422ea
Add permissions for actions on collections
sallybg Oct 22, 2024
550e24e
Allow deletion of a public collection
sallybg Oct 22, 2024
f45f1fc
Add methods for modification and deletion of collections
sallybg Oct 22, 2024
0ac71d8
Add documentation for select collection fields
sallybg Oct 22, 2024
7043085
Add tables to associate collections with data sets
sallybg Nov 26, 2024
a449b09
squash: remove collection association tables from collection.py
sallybg Nov 26, 2024
234d27a
squash: add collections and official collections to score set and exp…
sallybg Nov 26, 2024
c20de50
Add official collections to score set and experiment view models
sallybg Nov 26, 2024
73e3837
Add view models to request addition of data sets to collections
sallybg Nov 26, 2024
5ed82b5
Get my collections ordered by user role, and fix add data set to coll…
sallybg Nov 26, 2024
b4d6101
Add router to get limited user information by Orcid ID
sallybg Nov 26, 2024
d7151db
Automatically assign collection creator as admin
sallybg Dec 4, 2024
3d566bb
Only show collections' data sets for which user has read permissions
sallybg Dec 5, 2024
30860e1
Do not separately include owned collections in my collections
sallybg Dec 5, 2024
05f924e
Remove owned collections from view model
sallybg Dec 6, 2024
bce9b66
Change collection modify date when adding/removing a data set
sallybg Dec 6, 2024
d64f0f5
Filter data sets based on user permissions when fetching my collections
sallybg Dec 6, 2024
78b7e4a
Add users to Collection view model
sallybg Dec 6, 2024
54b6617
Collections routers: use body instead of query param to specify user …
sallybg Dec 11, 2024
3b451c5
Make all fields optional in body of update collection request, and al…
sallybg Dec 12, 2024
b31e69b
Make update collection a patch request. Fix bugs with some routers
sallybg Dec 18, 2024
3761bee
Add collection permissions to permissions API
sallybg Dec 18, 2024
1bc0edd
Include official collection names and URNs in experiment and score se…
jstone-dev Jan 1, 2025
eca53bd
Cleaned up TODOs and pluralized API paths for collection user permiss…
jstone-dev Jan 2, 2025
c45eb54
Changed the collection URN format.
jstone-dev Jan 2, 2025
f64bf22
Added collection unit tests.
jstone-dev Jan 2, 2025
c68e081
Formatted code with ruff.
jstone-dev Jan 2, 2025
f3fe567
Fixes for MyPy and preexisting unit tests
jstone-dev Jan 2, 2025
2ec050c
Cleanup and response to PR feedback
jstone-dev Jan 10, 2025
7642328
Add recordType Property to Collection View Models
bencap Jan 27, 2025
2b9acf3
Use Sequence type for Collection user lists
bencap Jan 27, 2025
ddc8f93
Merge pull request #374 from VariantEffect/feature/bencap/288/data-co…
bencap Jan 27, 2025
9733b91
Solve #230 All-NA hgvs columns in input problem. Remove NA columns fr…
EstelleDa Feb 10, 2025
6372515
Typo in mock_worker_variant_insertion leads to a wrong counts file path.
EstelleDa Feb 10, 2025
5ce410d
Merge pull request #384 from VariantEffect/estelle/debugATypo
EstelleDa Feb 10, 2025
fb6e690
Merge remote-tracking branch 'origin/release-2025.0.1' into estelle/d…
EstelleDa Feb 10, 2025
a9b35a6
Adjust return type position.
EstelleDa Feb 10, 2025
51d41b0
Modify the function and variables' names.
EstelleDa Feb 11, 2025
5749652
Merge pull request #385 from VariantEffect/estelle/dropDownloadNullHg…
EstelleDa Feb 11, 2025
16acb27
Add a pubmed url test.
EstelleDa Feb 12, 2025
5ad83b1
Remove print()
EstelleDa Feb 12, 2025
778c517
Place collections data model updates at alembic HEAD
bencap Feb 13, 2025
c267ede
Merge pull request #387 from VariantEffect/estelle/addPublicationIden…
EstelleDa Feb 14, 2025
8a4b426
Modify codes.
EstelleDa Feb 14, 2025
693ec58
Merge branch 'release-2025.0.1' into estelle/debugShowTmpSupersedingS…
EstelleDa Feb 14, 2025
7b4a29e
Remove unnecessary import.
EstelleDa Feb 14, 2025
86d6e03
Merge remote-tracking branch 'origin/estelle/debugShowTmpSupersedingS…
EstelleDa Feb 14, 2025
c95060d
Merge pull request #366 from VariantEffect/estelle/debugShowTmpSupers…
EstelleDa Feb 19, 2025
fe8af27
Merge branch 'release-2025.0.1' into release-2025.1.0
bencap Feb 25, 2025
4d62931
Bump version number
bencap Feb 25, 2025
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
29 changes: 29 additions & 0 deletions alembic/versions/aa73d39b3705_score_set_level_score_thresholds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""score set level score thresholds

Revision ID: aa73d39b3705
Revises: 68a0ec57694e
Create Date: 2024-11-13 11:23:57.917725

"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "aa73d39b3705"
down_revision = "68a0ec57694e"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("scoresets", sa.Column("score_calibrations", postgresql.JSONB(astext_type=sa.Text()), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("scoresets", "score_calibrations")
# ### end Alembic commands ###
117 changes: 117 additions & 0 deletions alembic/versions/c404b6719110_collections_data_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Collections data model

Revision ID: c404b6719110
Revises: aa73d39b3705
Create Date: 2024-10-15 12:57:29.682271

"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "c404b6719110"
down_revision = "aa73d39b3705"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"collections",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("urn", sa.String(length=64), nullable=True),
sa.Column("private", sa.Boolean(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("badge_name", sa.String(), nullable=True),
sa.Column("description", sa.String(), nullable=True),
sa.Column("created_by_id", sa.Integer(), nullable=True),
sa.Column("modified_by_id", sa.Integer(), nullable=True),
sa.Column("creation_date", sa.Date(), nullable=False),
sa.Column("modification_date", sa.Date(), nullable=False),
sa.ForeignKeyConstraint(
["created_by_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["modified_by_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_collections_created_by_id"), "collections", ["created_by_id"], unique=False)
op.create_index(op.f("ix_collections_modified_by_id"), "collections", ["modified_by_id"], unique=False)
op.create_index(op.f("ix_collections_urn"), "collections", ["urn"], unique=True)

op.create_table(
"collection_user_associations",
sa.Column("collection_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column(
"contribution_role",
sa.Enum(
"admin",
"editor",
"viewer",
name="contributionrole",
native_enum=False,
create_constraint=True,
length=32,
),
nullable=False,
),
sa.ForeignKeyConstraint(
["collection_id"],
["collections.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("collection_id", "user_id"),
)

op.create_table(
"collection_experiments",
sa.Column("collection_id", sa.Integer(), nullable=False),
sa.Column("experiment_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["collection_id"],
["collections.id"],
),
sa.ForeignKeyConstraint(
["experiment_id"],
["experiments.id"],
),
sa.PrimaryKeyConstraint("collection_id", "experiment_id"),
)

op.create_table(
"collection_score_sets",
sa.Column("collection_id", sa.Integer(), nullable=False),
sa.Column("score_set_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["collection_id"],
["collections.id"],
),
sa.ForeignKeyConstraint(
["score_set_id"],
["scoresets.id"],
),
sa.PrimaryKeyConstraint("collection_id", "score_set_id"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("collection_score_sets")
op.drop_table("collection_experiments")
op.drop_table("collection_user_associations")
op.drop_index(op.f("ix_collections_urn"), table_name="collections")
op.drop_index(op.f("ix_collections_modified_by_id"), table_name="collections")
op.drop_index(op.f("ix_collections_created_by_id"), table_name="collections")
op.drop_table("collections")
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "mavedb"
version = "2025.0.0"
version = "2025.1.0"
description = "API for MaveDB, the database of Multiplexed Assays of Variant Effect."
license = "AGPL-3.0-only"
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/mavedb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
logger = module_logging.getLogger(__name__)

__project__ = "mavedb-api"
__version__ = "2025.0.0"
__version__ = "2025.1.0"

logger.info(f"MaveDB {__version__}")
9 changes: 7 additions & 2 deletions src/mavedb/lib/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from mavedb.models.contributor import Contributor
from mavedb.models.controlled_keyword import ControlledKeyword
from mavedb.models.experiment import Experiment
from mavedb.models.experiment_controlled_keyword import ExperimentControlledKeywordAssociation
from mavedb.models.experiment_controlled_keyword import (
ExperimentControlledKeywordAssociation,
)
from mavedb.models.publication_identifier import PublicationIdentifier
from mavedb.models.score_set import ScoreSet
from mavedb.models.user import User
Expand Down Expand Up @@ -117,6 +119,9 @@ def search_experiments(
items = []

save_to_logging_context({"matching_resources": len(items)})
logger.debug(msg="Experiment search yielded {len(items)} matching resources.", extra=logging_context())
logger.debug(
msg="Experiment search yielded {len(items)} matching resources.",
extra=logging_context(),
)

return items
138 changes: 136 additions & 2 deletions src/mavedb/lib/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from mavedb.db.base import Base
from mavedb.lib.authentication import UserData
from mavedb.lib.logging.context import logging_context, save_to_logging_context
from mavedb.models.collection import Collection
from mavedb.models.enums.contribution_role import ContributionRole
from mavedb.models.enums.user_role import UserRole
from mavedb.models.experiment import Experiment
from mavedb.models.experiment_set import ExperimentSet
Expand All @@ -15,6 +17,7 @@


class Action(Enum):
LOOKUP = "lookup"
READ = "read"
UPDATE = "update"
DELETE = "delete"
Expand All @@ -23,6 +26,7 @@ class Action(Enum):
SET_SCORES = "set_scores"
ADD_ROLE = "add_role"
PUBLISH = "publish"
ADD_BADGE = "add_badge"


class PermissionResponse:
Expand All @@ -33,9 +37,15 @@ def __init__(self, permitted: bool, http_code: int = 403, message: Optional[str]

save_to_logging_context({"permission_message": self.message, "access_permitted": self.permitted})
if self.permitted:
logger.debug(msg="Access to the requested resource is permitted.", extra=logging_context())
logger.debug(
msg="Access to the requested resource is permitted.",
extra=logging_context(),
)
else:
logger.debug(msg="Access to the requested resource is not permitted.", extra=logging_context())
logger.debug(
msg="Access to the requested resource is not permitted.",
extra=logging_context(),
)


class PermissionException(Exception):
Expand All @@ -59,6 +69,7 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) ->
user_is_owner = False
user_is_self = False
user_may_edit = False
user_may_view_private = False
active_roles = user_data.active_roles if user_data else []

if isinstance(item, ExperimentSet) or isinstance(item, Experiment) or isinstance(item, ScoreSet):
Expand All @@ -72,6 +83,27 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) ->

save_to_logging_context({"resource_is_published": published})

if isinstance(item, Collection):
assert item.private is not None
private = item.private
published = item.private is False
user_is_owner = item.created_by_id == user_data.user.id if user_data is not None else False
admin_user_ids = set()
editor_user_ids = set()
viewer_user_ids = set()
for user_association in item.user_associations:
if user_association.contribution_role == ContributionRole.admin:
admin_user_ids.add(user_association.user_id)
elif user_association.contribution_role == ContributionRole.editor:
editor_user_ids.add(user_association.user_id)
elif user_association.contribution_role == ContributionRole.viewer:
viewer_user_ids.add(user_association.user_id)
user_is_admin = user_is_owner or (user_data is not None and user_data.user.id in admin_user_ids)
user_may_edit = user_is_admin or (user_data is not None and user_data.user.id in editor_user_ids)
user_may_view_private = user_may_edit or (user_data is not None and (user_data.user.id in viewer_user_ids))

save_to_logging_context({"resource_is_published": published})

if isinstance(item, User):
user_is_self = item.id == user_data.user.id if user_data is not None else False
user_may_edit = user_is_self
Expand Down Expand Up @@ -254,7 +286,109 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) ->
else:
raise NotImplementedError(f"has_permission(User, ScoreSet, {action}, Role)")

elif isinstance(item, Collection):
if action == Action.READ:
if user_may_view_private or not private:
return PermissionResponse(True)
# Roles which may perform this operation.
elif roles_permitted(active_roles, [UserRole.admin]):
return PermissionResponse(True)
elif private:
# Do not acknowledge the existence of a private entity.
return PermissionResponse(False, 404, f"collection with URN '{item.urn}' not found")
elif user_data is None or user_data.user is None:
return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'")
else:
return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'")
elif action == Action.UPDATE:
if user_may_edit:
return PermissionResponse(True)
# Roles which may perform this operation.
elif roles_permitted(active_roles, [UserRole.admin]):
return PermissionResponse(True)
elif private and not user_may_view_private:
# Do not acknowledge the existence of a private entity.
return PermissionResponse(False, 404, f"score set with URN '{item.urn}' not found")
elif user_data is None or user_data.user is None:
return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'")
else:
return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'")
elif action == Action.DELETE:
# A collection may be deleted even if it has been published, as long as it is not an official collection.
if user_is_owner:
return PermissionResponse(
not item.badge_name,
403,
f"insufficient permissions for URN '{item.urn}'",
)
# MaveDB admins may delete official collections.
elif roles_permitted(active_roles, [UserRole.admin]):
return PermissionResponse(True)
elif private and not user_may_view_private:
# Do not acknowledge the existence of a private entity.
return PermissionResponse(False, 404, f"collection with URN '{item.urn}' not found")
else:
return PermissionResponse(False)
elif action == Action.PUBLISH:
if user_is_admin:
return PermissionResponse(True)
elif roles_permitted(active_roles, []):
return PermissionResponse(True)
elif private and not user_may_view_private:
# Do not acknowledge the existence of a private entity.
return PermissionResponse(False, 404, f"score set with URN '{item.urn}' not found")
else:
return PermissionResponse(False)
elif action == Action.ADD_SCORE_SET:
# Whether the collection is private or public, only permitted users can add a score set to a collection.
if user_may_edit or roles_permitted(active_roles, [UserRole.admin]):
return PermissionResponse(True)
elif private and not user_may_view_private:
return PermissionResponse(False, 404, f"collection with URN '{item.urn}' not found")
else:
return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'")
elif action == Action.ADD_EXPERIMENT:
# Only permitted users can add an experiment to an existing collection.
return PermissionResponse(
user_may_edit or roles_permitted(active_roles, [UserRole.admin]),
404 if private and not user_may_view_private else 403,
(
f"collection with URN '{item.urn}' not found"
if private and not user_may_view_private
else f"insufficient permissions for URN '{item.urn}'"
),
)
elif action == Action.ADD_ROLE:
# Both collection admins and MaveDB admins can add a user to a collection role
if user_is_admin or roles_permitted(active_roles, [UserRole.admin]):
return PermissionResponse(True)
else:
return PermissionResponse(False, 403, "Insufficient permissions to add user role.")
# only MaveDB admins may add a badge name to a collection, which makes the collection considered "official"
elif action == Action.ADD_BADGE:
# Roles which may perform this operation.
if roles_permitted(active_roles, [UserRole.admin]):
return PermissionResponse(True)
elif private:
# Do not acknowledge the existence of a private entity.
return PermissionResponse(False, 404, f"collection with URN '{item.urn}' not found")
elif user_data is None or user_data.user is None:
return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'")
else:
return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'")
else:
raise NotImplementedError(f"has_permission(User, ScoreSet, {action}, Role)")

elif isinstance(item, User):
if action == Action.LOOKUP:
# any existing user can look up any mavedb user by Orcid ID
# lookup differs from read because lookup means getting the first name, last name, and orcid ID of the user,
# while read means getting an admin view of the user's details
if user_data is not None and user_data.user is not None:
return PermissionResponse(True)
else:
# TODO is this inappropriately acknowledging the existence of the user?
return PermissionResponse(False, 401, "Insufficient permissions for user lookup.")
if action == Action.READ:
if user_is_self:
return PermissionResponse(True)
Expand Down
Loading
Loading