diff --git a/src/mavedb/routers/score_sets.py b/src/mavedb/routers/score_sets.py index 7376ca4b1..a8cd19684 100644 --- a/src/mavedb/routers/score_sets.py +++ b/src/mavedb/routers/score_sets.py @@ -672,6 +672,54 @@ def search_my_score_sets( return {"score_sets": enriched_score_sets, "num_score_sets": num_score_sets} +RECENTLY_PUBLISHED_SCORE_SETS_MAX_LIMIT = 20 + + +@router.get( + "/score-sets/recently-published", + status_code=200, + response_model=list[score_set.ScoreSet], + response_model_exclude_none=True, + summary="List recently published score sets", +) +def list_recently_published_score_sets( + limit: int = Query( + default=10, + ge=1, + le=RECENTLY_PUBLISHED_SCORE_SETS_MAX_LIMIT, + description=f"Number of score sets to return (maximum {RECENTLY_PUBLISHED_SCORE_SETS_MAX_LIMIT}).", + ), + db: Session = Depends(deps.get_db), + user_data: Optional[UserData] = Depends(get_current_user), +) -> Any: + """ + Return the most recently published score sets, ordered by publication date descending. + """ + save_to_logging_context({"requested_resource": "recently-published", "limit": limit}) + + items = ( + db.query(ScoreSet) + .filter(ScoreSet.published_date.isnot(None), ScoreSet.private.is_(False)) + .order_by(ScoreSet.published_date.desc(), ScoreSet.urn.desc()) + .limit(limit) + .all() + ) + + result = [] + for item in items: + if not has_permission(user_data, item, Action.READ).permitted: + continue + if ( + item.superseding_score_set + and not has_permission(user_data, item.superseding_score_set, Action.READ).permitted + ): + item.superseding_score_set = None + enriched_experiment = enrich_experiment_with_num_score_sets(item.experiment, user_data) + result.append(score_set.ScoreSet.model_validate(item).copy(update={"experiment": enriched_experiment})) + + return result + + @router.get( "/score-sets/{urn}", status_code=200, diff --git a/tests/helpers/util/score_set.py b/tests/helpers/util/score_set.py index b2a8b2c64..cd9f1bedc 100644 --- a/tests/helpers/util/score_set.py +++ b/tests/helpers/util/score_set.py @@ -165,9 +165,9 @@ def create_seq_score_set_with_variants( count_columns_metadata_json_path, ) - assert score_set["numVariants"] == 3, ( - f"Could not create sequence based score set with variants within experiment {experiment_urn}" - ) + assert ( + score_set["numVariants"] == 3 + ), f"Could not create sequence based score set with variants within experiment {experiment_urn}" jsonschema.validate(instance=score_set, schema=ScoreSet.model_json_schema()) return score_set @@ -196,9 +196,9 @@ def create_acc_score_set_with_variants( count_columns_metadata_json_path, ) - assert score_set["numVariants"] == 3, ( - f"Could not create sequence based score set with variants within experiment {experiment_urn}" - ) + assert ( + score_set["numVariants"] == 3 + ), f"Could not create sequence based score set with variants within experiment {experiment_urn}" jsonschema.validate(instance=score_set, schema=ScoreSet.model_json_schema()) return score_set diff --git a/tests/routers/test_experiments.py b/tests/routers/test_experiments.py index 1a04ed6a2..2b6be3b5d 100644 --- a/tests/routers/test_experiments.py +++ b/tests/routers/test_experiments.py @@ -363,10 +363,9 @@ def test_cannot_create_experiment_that_keywords_has_endogenous_without_method_me assert response.status_code == 422 response_data = response.json() assert ( - response_data["detail"] - == "If 'Variant Library Creation Method' is 'Endogenous locus library method', " - "both 'Endogenous Locus Library Method System' and 'Endogenous Locus Library Method Mechanism' " - "must be present." + response_data["detail"] == "If 'Variant Library Creation Method' is 'Endogenous locus library method', " + "both 'Endogenous Locus Library Method System' and 'Endogenous Locus Library Method Mechanism' " + "must be present." ) @@ -401,10 +400,9 @@ def test_cannot_create_experiment_that_keywords_has_endogenous_without_method_sy assert response.status_code == 422 response_data = response.json() assert ( - response_data["detail"] - == "If 'Variant Library Creation Method' is 'Endogenous locus library method', " - "both 'Endogenous Locus Library Method System' and 'Endogenous Locus Library Method Mechanism' " - "must be present." + response_data["detail"] == "If 'Variant Library Creation Method' is 'Endogenous locus library method', " + "both 'Endogenous Locus Library Method System' and 'Endogenous Locus Library Method Mechanism' " + "must be present." ) @@ -478,10 +476,9 @@ def test_cannot_create_experiment_that_keywords_has_in_vitro_without_method_syst assert response.status_code == 422 response_data = response.json() assert ( - response_data["detail"] - == "If 'Variant Library Creation Method' is 'In vitro construct library method', " - "both 'In Vitro Construct Library Method System' and 'In Vitro Construct Library Method Mechanism' " - "must be present." + response_data["detail"] == "If 'Variant Library Creation Method' is 'In vitro construct library method', " + "both 'In Vitro Construct Library Method System' and 'In Vitro Construct Library Method Mechanism' " + "must be present." ) @@ -516,10 +513,9 @@ def test_cannot_create_experiment_that_keywords_has_in_vitro_without_method_mech assert response.status_code == 422 response_data = response.json() assert ( - response_data["detail"] - == "If 'Variant Library Creation Method' is 'In vitro construct library method', " - "both 'In Vitro Construct Library Method System' and 'In Vitro Construct Library Method Mechanism' " - "must be present." + response_data["detail"] == "If 'Variant Library Creation Method' is 'In vitro construct library method', " + "both 'In Vitro Construct Library Method System' and 'In Vitro Construct Library Method Mechanism' " + "must be present." ) @@ -717,23 +713,28 @@ def test_update_experiment_keywords(session, client, setup_router_db): assert response.status_code == 200 experiment = response.json() experiment_post_payload = experiment.copy() - experiment_post_payload.update({"keywords": [ + experiment_post_payload.update( { - "keyword": { - "key": "Phenotypic Assay Profiling Strategy", - "label": "Shotgun sequencing", - "special": False, - "description": "Description" - }, - "description": "Details of phenotypic assay profiling strategy", - }, - - ]}) + "keywords": [ + { + "keyword": { + "key": "Phenotypic Assay Profiling Strategy", + "label": "Shotgun sequencing", + "special": False, + "description": "Description", + }, + "description": "Details of phenotypic assay profiling strategy", + }, + ] + } + ) updated_response = client.put(f"/api/v1/experiments/{experiment['urn']}", json=experiment_post_payload) assert updated_response.status_code == 200 updated_experiment = updated_response.json() updated_expected_response = deepcopy(TEST_EXPERIMENT_WITH_UPDATE_KEYWORD_RESPONSE) - updated_expected_response.update({"urn": updated_experiment["urn"], "experimentSetUrn": updated_experiment["experimentSetUrn"]}) + updated_expected_response.update( + {"urn": updated_experiment["urn"], "experimentSetUrn": updated_experiment["experimentSetUrn"]} + ) assert sorted(updated_expected_response.keys()) == sorted(updated_experiment.keys()) for key in updated_experiment: assert (key, updated_expected_response[key]) == (key, updated_experiment[key]) @@ -745,12 +746,21 @@ def test_update_experiment_keywords_case_insensitive(session, client, setup_rout experiment = create_experiment(client) experiment_post_payload = experiment.copy() # Test database has Delivery Method. The updating keyword's key is delivery method. - experiment_post_payload.update({"keywords": [ + experiment_post_payload.update( { - "keyword": {"key": "delivery method", "label": "Other", "special": False, "description": "Description"}, - "description": "Details of delivery method", - }, - ]}) + "keywords": [ + { + "keyword": { + "key": "delivery method", + "label": "Other", + "special": False, + "description": "Description", + }, + "description": "Details of delivery method", + }, + ] + } + ) response = client.put(f"/api/v1/experiments/{experiment['urn']}", json=experiment_post_payload) response_data = response.json() expected_response = deepcopy(TEST_EXPERIMENT_WITH_KEYWORD_RESPONSE) diff --git a/tests/routers/test_score_set.py b/tests/routers/test_score_set.py index f412b16a2..72615cebf 100644 --- a/tests/routers/test_score_set.py +++ b/tests/routers/test_score_set.py @@ -1414,6 +1414,96 @@ def test_cannot_publish_score_set_without_variants(client, setup_router_db): assert "cannot publish score set without variant scores" in response_data["detail"] +######################################################################################################################## +# Recently published score sets +######################################################################################################################## + + +def test_recently_published_returns_empty_list_when_no_score_sets_published(client, setup_router_db): + response = client.get("/api/v1/score-sets/recently-published") + assert response.status_code == 200 + assert response.json() == [] + + +def test_recently_published_returns_published_score_sets(session, data_provider, client, setup_router_db, data_files): + experiment = create_experiment(client) + score_set_1 = create_seq_score_set(client, experiment["urn"]) + score_set_1 = mock_worker_variant_insertion(client, session, data_provider, score_set_1, data_files / "scores.csv") + score_set_2 = create_seq_score_set(client, experiment["urn"]) + score_set_2 = mock_worker_variant_insertion(client, session, data_provider, score_set_2, data_files / "scores.csv") + + with patch.object(arq.ArqRedis, "enqueue_job", return_value=None): + published_1 = publish_score_set(client, score_set_1["urn"]) + published_2 = publish_score_set(client, score_set_2["urn"]) + + response = client.get("/api/v1/score-sets/recently-published") + assert response.status_code == 200 + response_data = response.json() + + returned_urns = [ss["urn"] for ss in response_data] + assert published_1["urn"] in returned_urns + assert published_2["urn"] in returned_urns + + +def test_recently_published_does_not_return_unpublished_score_sets(client, setup_router_db): + experiment = create_experiment(client) + create_seq_score_set(client, experiment["urn"]) + + response = client.get("/api/v1/score-sets/recently-published") + assert response.status_code == 200 + assert response.json() == [] + + +def test_recently_published_respects_limit_parameter(session, data_provider, client, setup_router_db, data_files): + experiment = create_experiment(client) + # Create and upload variants for all score sets before publishing any, because publishing + # changes the experiment URN from a tmp URN to a permanent URN. + score_sets = [] + for _ in range(3): + score_set = create_seq_score_set(client, experiment["urn"]) + score_set = mock_worker_variant_insertion(client, session, data_provider, score_set, data_files / "scores.csv") + score_sets.append(score_set) + + published_urns = [] + with patch.object(arq.ArqRedis, "enqueue_job", return_value=None): + for score_set in score_sets: + published = publish_score_set(client, score_set["urn"]) + published_urns.append(published["urn"]) + + response = client.get("/api/v1/score-sets/recently-published?limit=2") + assert response.status_code == 200 + response_data = response.json() + assert len(response_data) == 2 + + +def test_recently_published_rejects_limit_exceeding_maximum(client, setup_router_db): + response = client.get("/api/v1/score-sets/recently-published?limit=21") + assert response.status_code == 422 + + +def test_recently_published_rejects_limit_of_zero(client, setup_router_db): + response = client.get("/api/v1/score-sets/recently-published?limit=0") + assert response.status_code == 422 + + +def test_recently_published_accessible_to_anonymous_user( + session, data_provider, client, setup_router_db, data_files, anonymous_app_overrides +): + experiment = create_experiment(client) + score_set = create_seq_score_set(client, experiment["urn"]) + score_set = mock_worker_variant_insertion(client, session, data_provider, score_set, data_files / "scores.csv") + + with patch.object(arq.ArqRedis, "enqueue_job", return_value=None): + published = publish_score_set(client, score_set["urn"]) + + with DependencyOverrider(anonymous_app_overrides): + response = client.get("/api/v1/score-sets/recently-published") + + assert response.status_code == 200 + returned_urns = [ss["urn"] for ss in response.json()] + assert published["urn"] in returned_urns + + def test_cannot_publish_other_user_private_score_set(session, data_provider, client, setup_router_db, data_files): experiment = create_experiment(client) score_set = create_seq_score_set(client, experiment["urn"])