diff --git a/src/providers/views.py b/src/providers/views.py index eb08c6242bcdfd2b374db046b5d629afd48a104a..462598af26736975f58a0049ab14210548d2594a 100644 --- a/src/providers/views.py +++ b/src/providers/views.py @@ -335,6 +335,52 @@ class CheckAnalyticsTokenNameAvailable(APIView): ) +def get_system_statement_query(providers=[]): + """ + :param providers: optional list of providers to filter system statements by + :return: query object which filters for system statements and optionally the supplied providers + """ + query = {"$and": [ + {"actor.mbox": {"$exists": True}} + ]} + if len(providers) > 0: + provider_filters = [] + for provider in providers: + provider_filters.append("system:" + str(provider.id)) + query["$and"].append({"actor.mbox": {"$in": provider_filters}}) + else: + query["$and"].append({"actor.mbox": {"$regex": "^system"}}) + + return query + + +def get_system_statements(collection, providers=[]): + """ + Filters statements in LRS db by special system user and optionally a list of providers. + + :param providers: optional list of provider objects used for selecting relevant system statements + :param collection: lrs database containing xapi statements + :return: all relevant statements + """ + + system_statement_query = get_system_statement_query(providers) + return collection.find(system_statement_query) + + +def replace_provider_id(statement, providers): + if statement.get("actor", {}).get("mbox", "").startswith("system:"): + provider_id = int((statement.get("actor", {}).get("mbox", "").split("system:"))[1]) + provider = next((x for x in providers if x.id == provider_id), None) + if provider: + statement["actor"]["mbox"] = "system:" + provider.name + else: + # fallback - return unmodified statement + return statement + else: + return statement + + + class GetProviderData(APIView): """ Endpoint that allows an analytics engine to obtain provider statements from the lrs. @@ -403,7 +449,7 @@ class GetProviderData(APIView): anon_verbs.append(verb) # selects for anonymized statements of which there are enough different actors - # or explicitly non-anonymized statements + # or explicitly non-anonymized statements / system statements anon_query = {"$or": [ # current statement is anonymized and # enough anonymized statements for this verb exist @@ -418,7 +464,9 @@ class GetProviderData(APIView): {"$and": [ {"actor.mbox": {"$exists": False}}, {"actor.mbox": {"$regex": "^mailto"}}, - ]} + ]}, + # also query for system statements added by relevant providers + get_system_statement_query(providers) ]} ]} @@ -443,7 +491,7 @@ class GetProviderData(APIView): cursor = collection.find(query).limit(page_size) data = { "verbs": list(set(active_verbs)), - "statements": list(cursor), + "statements": list(map(replace_provider_id, list(cursor))), "page_size": page_size, } diff --git a/src/xapi/tests/tests.py b/src/xapi/tests/tests.py index 1279ad164e23af1a3422257304eec707397c844b..4467c61f5627b96c6fad2bd447ec25ee79594841 100644 --- a/src/xapi/tests/tests.py +++ b/src/xapi/tests/tests.py @@ -2,6 +2,7 @@ import json import os from datetime import datetime from io import StringIO +from unittest import mock from unittest.mock import patch from django.core.exceptions import ObjectDoesNotExist @@ -173,6 +174,43 @@ class XAPITestCase(TestCase): # ) # self.assertEqual(response.status_code, 403) + @patch("xapi.views.store_in_db", mock_store_in_lrs) + def test_system_user(self): + try: + provider = Provider.objects.order_by("id").first() + except ObjectDoesNotExist: + self.assertTrue(False) # provider was not created when uploading schema + + keys = [ + auth.key for auth in ProviderAuthorization.objects.filter(provider=provider) + ] + self.assertTrue(len(keys) > 0) # keys were not created + key = keys[0] + + provider_client = APIClient() + provider_client.credentials(HTTP_AUTHORIZATION="Basic " + key) + + + response = provider_client.post( + "/xapi/statements", + { + "actor": {"mbox": f"system:{provider.id}"}, + "verb": {"id": "some_id"}, + "object": { + "id": "some_other_id", + "objectType": "Activity", + "definition": "object_definition", + }, + "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + format="json", + ) + self.assertEqual(response.status_code, 200) + + # {'message': 'processed', 'provider': 'H5P'} + self.assertEqual( + response.json()["message"], "xAPI statements successfully stored in LRS" + ) class TestxAPIWithDataRecordingPause(BaseTestCase): @patch("xapi.views.store_in_db", mock_store_in_lrs) diff --git a/src/xapi/views.py b/src/xapi/views.py index 3c4550f7f427d37a8ecb7d95815f65241f97a060..4af3b782062a9b77b3e71a87599925342ff3a361 100644 --- a/src/xapi/views.py +++ b/src/xapi/views.py @@ -56,6 +56,13 @@ def process_statement(x_api_statement, provider, latest_schema): except ValidationError as e: return {"valid": False, "accepted": False, "reason": e.message} + # system statements are directly stored without checking for consent + if x_api_statement.get("actor", {}).get("mbox", "").startswith("system:"): + if int((x_api_statement.get("actor", {}).get("mbox", "").split("system:"))[1]) == provider.id: + return {"valid": True, "accepted": True} + else: + return {"valid": False, "accepted": False, "reason": "Wrong provider ID"} + mbox = dict( enumerate(x_api_statement.get("actor", {}).get("mbox", "").split("mailto:")) ).get(1) @@ -85,7 +92,7 @@ def process_statement(x_api_statement, provider, latest_schema): # essential verbs do not require consent if not verb in [verb["id"] for verb in latest_schema.essential_verbs]: - + anon_verbs = [verb["id"] for verblist in [group["verbs"] for group in latest_schema.groups] for verb in verblist if verb.get("allowAnonymizedCollection", False)] # has the user paused data collection altogether?