From f972e63555939bb43a02daac2de9947e698323e9 Mon Sep 17 00:00:00 2001 From: "andrew.harper2" Date: Mon, 3 Aug 2026 10:52:34 +0100 Subject: [PATCH 1/3] Add community reader invite script --- .../login/invite_community_reader.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 biosimdb_interface/login/invite_community_reader.py diff --git a/biosimdb_interface/login/invite_community_reader.py b/biosimdb_interface/login/invite_community_reader.py new file mode 100644 index 0000000..7fec11a --- /dev/null +++ b/biosimdb_interface/login/invite_community_reader.py @@ -0,0 +1,20 @@ +import os +import requests +user_id = "ADD ID" +slug = "biosimdb" +base = "https://data-collections-dev.psdi.ac.uk" +token = "ADD TOKEN" +headers = {"Authorization": "Bearer " + token} +r = requests.get(base + "/api/communities/" + slug, headers=headers) +community_id = r.json()["id"] +r = requests.get(base + "/api/communities/" + community_id + "/members", params={"size": 1000}, headers=headers) +found = False +for m in r.json()["hits"]["hits"]: + if m["member"]["id"] == user_id: + found = True +if found: + print("member of " + slug) +else: + data = {"members": [{"id": user_id, "type": "user"}], "role": "reader"} + requests.post(base + "/api/communities/" + community_id + "/invitations", json=data, headers=headers) + print("invited user " + user_id + " to " + slug) \ No newline at end of file From 954445e0e86620d06de0a11362c6c8153f3c3cdf Mon Sep 17 00:00:00 2001 From: Jas Kalayan Date: Mon, 3 Aug 2026 19:52:02 +0100 Subject: [PATCH 2/3] integrate user invite into app and update tests --- biosimdb_interface/form/webform.py | 9 +- .../login/invite_community_reader.py | 83 ++++++++++++++----- tests/test_form/test_upload.py | 9 +- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/biosimdb_interface/form/webform.py b/biosimdb_interface/form/webform.py index 941f730..a828b47 100644 --- a/biosimdb_interface/form/webform.py +++ b/biosimdb_interface/form/webform.py @@ -16,6 +16,7 @@ ) from werkzeug.datastructures import ImmutableMultiDict +from biosimdb_interface.login.invite_community_reader import invite_user from biosimdb_interface.schema.webform import WEBFORM_SCHEMA, get_simulation_metadata from . import form_bp @@ -125,8 +126,10 @@ def resume_submit(): @form_bp.route("/do_submit", methods=["POST"]) def do_submit(): """Execute the deferred Invenio upload using session-stored form data. - Called automatically by the loading page after login. Clears pending - session data after upload and renders the success page with the record URL. + Called automatically by the loading page after login. + Automatically invite user to Invenio instance community, then submit. + Clears pending session data after upload and renders the success page + with the record URL. """ form_data = session.pop("pending_form_data", None) tmpdir = session.pop("pending_files_dir", None) @@ -140,6 +143,8 @@ def do_submit(): ) try: + token = session.get("access_token") + invite_user("biosimdb", token) draft_id = prepare_for_invenio(flat_form, tmpdir) except requests.HTTPError as exc: status = exc.response.status_code if exc.response is not None else None diff --git a/biosimdb_interface/login/invite_community_reader.py b/biosimdb_interface/login/invite_community_reader.py index 7fec11a..8989be2 100644 --- a/biosimdb_interface/login/invite_community_reader.py +++ b/biosimdb_interface/login/invite_community_reader.py @@ -1,20 +1,65 @@ -import os +#!/usr/bin/env python +"""Automatically invite logged in user to BioSimDB.""" + import requests -user_id = "ADD ID" -slug = "biosimdb" -base = "https://data-collections-dev.psdi.ac.uk" -token = "ADD TOKEN" -headers = {"Authorization": "Bearer " + token} -r = requests.get(base + "/api/communities/" + slug, headers=headers) -community_id = r.json()["id"] -r = requests.get(base + "/api/communities/" + community_id + "/members", params={"size": 1000}, headers=headers) -found = False -for m in r.json()["hits"]["hits"]: - if m["member"]["id"] == user_id: - found = True -if found: - print("member of " + slug) -else: - data = {"members": [{"id": user_id, "type": "user"}], "role": "reader"} - requests.post(base + "/api/communities/" + community_id + "/invitations", json=data, headers=headers) - print("invited user " + user_id + " to " + slug) \ No newline at end of file +from flask import current_app + + +def _fetch_user_id(access_token: str): + """ + Fetch the logged in user ID. + Args: + access_token (str): OAuth2 bearer token for the authenticated user. + + Returns: + int | None: Invenio instance user ID if found, or None. + + """ + api_base = current_app.config.get("API_BASE", "").rstrip("/") + url = f"{api_base}/me" + headers = {"Authorization": f"Bearer {access_token}"} + resp = requests.get(url, headers=headers, timeout=10) + data = resp.json() + + if isinstance(data, dict): + if data.get("id"): + return data["id"] + else: + return None + + +def invite_user(slug: str, access_token: str): + """ + Check if a logged in user is a member of an Invenio instance community. + Add the user if they are not a member of biosimdb. + + Args: + user_id (int): Invenio instance user ID. + slug (str): Name of the community in the Invenio instance. + access_token (str): OAuth2 bearer token for the authenticated user. + """ + user_id = _fetch_user_id(access_token) + api_base = current_app.config.get("API_BASE", "").rstrip("/") + headers = {"Authorization": f"Bearer {access_token}"} + + r = requests.get(api_base + "/communities/" + slug, headers=headers) + community_id = r.json()["id"] + + r = requests.get( + api_base + "/communities/" + community_id + "/members", + params={"size": 1000}, + headers=headers, + ) + found = False + for m in r.json()["hits"]["hits"]: + if m["member"]["id"] == str(user_id): + found = True + if found: + pass + else: + data = {"members": [{"id": user_id, "type": "user"}], "role": "reader"} + requests.post( + api_base + "/communities/" + community_id + "/invitations", + json=data, + headers=headers, + ) diff --git a/tests/test_form/test_upload.py b/tests/test_form/test_upload.py index e04ebc9..8c70a4a 100644 --- a/tests/test_form/test_upload.py +++ b/tests/test_form/test_upload.py @@ -51,8 +51,11 @@ def test_do_submit_calls_invenio(client): sess["pending_form_data"] = {"simulation_name": ["test"]} sess["pending_files_dir"] = "/tmp/fake_pending" - with patch("biosimdb_interface.form.webform.prepare_for_invenio") as mock_prepare: + with ( + patch("biosimdb_interface.form.webform.invite_user") as mock_invite, + patch("biosimdb_interface.form.webform.prepare_for_invenio") as mock_prepare, + ): mock_prepare.return_value = "draft-123" - response = client.post("/do_submit") + _response = client.post("/do_submit") + assert mock_invite.called assert mock_prepare.called - assert response.status_code in (200, 302) From 5c638dd37c144a4e4df448fc40187e331d928c52 Mon Sep 17 00:00:00 2001 From: Jas Kalayan Date: Mon, 3 Aug 2026 19:56:31 +0100 Subject: [PATCH 3/3] add tests for community invite --- biosimdb_interface/form/webform.py | 2 +- ...ommunity_reader.py => community_invite.py} | 0 tests/test_login/test_community_invite.py | 77 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) rename biosimdb_interface/login/{invite_community_reader.py => community_invite.py} (100%) create mode 100644 tests/test_login/test_community_invite.py diff --git a/biosimdb_interface/form/webform.py b/biosimdb_interface/form/webform.py index a828b47..07b2bfd 100644 --- a/biosimdb_interface/form/webform.py +++ b/biosimdb_interface/form/webform.py @@ -16,7 +16,7 @@ ) from werkzeug.datastructures import ImmutableMultiDict -from biosimdb_interface.login.invite_community_reader import invite_user +from biosimdb_interface.login.community_invite import invite_user from biosimdb_interface.schema.webform import WEBFORM_SCHEMA, get_simulation_metadata from . import form_bp diff --git a/biosimdb_interface/login/invite_community_reader.py b/biosimdb_interface/login/community_invite.py similarity index 100% rename from biosimdb_interface/login/invite_community_reader.py rename to biosimdb_interface/login/community_invite.py diff --git a/tests/test_login/test_community_invite.py b/tests/test_login/test_community_invite.py new file mode 100644 index 0000000..6adc57c --- /dev/null +++ b/tests/test_login/test_community_invite.py @@ -0,0 +1,77 @@ +from unittest.mock import Mock, patch + +import pytest + +from biosimdb_interface.login import community_invite as mod + + +def _resp(payload): + r = Mock() + r.json.return_value = payload + return r + + +def test_fetch_user_id_returns_id(app): + with ( + app.app_context(), + patch("biosimdb_interface.login.community_invite.requests.get") as get, + ): + get.return_value = _resp({"id": 7}) + assert mod._fetch_user_id("tok") == 7 + get.assert_called_once_with( + "http://localhost/api/me", + headers={"Authorization": "Bearer tok"}, + timeout=10, + ) + + +@pytest.mark.parametrize("payload", [[], {"username": "x"}]) +def test_fetch_user_id_returns_none_for_non_id_payloads(app, payload): + with ( + app.app_context(), + patch("biosimdb_interface.login.community_invite.requests.get") as get, + ): + get.return_value = _resp(payload) + assert mod._fetch_user_id("tok") is None + + +def test_invite_user_does_not_post_if_member_exists(app): + with ( + app.app_context(), + patch( + "biosimdb_interface.login.community_invite._fetch_user_id", return_value=42 + ), + patch("biosimdb_interface.login.community_invite.requests.get") as get, + patch("biosimdb_interface.login.community_invite.requests.post") as post, + ): + get.side_effect = [ + _resp({"id": "comm-1"}), + _resp({"hits": {"hits": [{"member": {"id": "42"}}]}}), + ] + + mod.invite_user("biosimdb", "tok") + + post.assert_not_called() + + +def test_invite_user_posts_if_member_missing(app): + with ( + app.app_context(), + patch( + "biosimdb_interface.login.community_invite._fetch_user_id", return_value=42 + ), + patch("biosimdb_interface.login.community_invite.requests.get") as get, + patch("biosimdb_interface.login.community_invite.requests.post") as post, + ): + get.side_effect = [ + _resp({"id": "comm-1"}), + _resp({"hits": {"hits": [{"member": {"id": "99"}}]}}), + ] + + mod.invite_user("biosimdb", "tok") + + post.assert_called_once_with( + "http://localhost/api/communities/comm-1/invitations", + json={"members": [{"id": 42, "type": "user"}], "role": "reader"}, + headers={"Authorization": "Bearer tok"}, + )