From 4efc1b27e60554acf73c476dbf1e208deb3ab719 Mon Sep 17 00:00:00 2001
From: Lennard Strohmeyer <lennard.strohmeyer@digitallearning.gmbh>
Date: Wed, 27 Nov 2024 16:51:51 +0100
Subject: [PATCH] #106: Weiterleitung an weitere(s) LRS

---
 docs/docs/provider_schema.md                  | 28 +++++++++++++++++--
 src/consents/serializers.py                   |  2 +-
 src/consents/views.py                         |  2 ++
 src/fixtures/initial_db.json                  |  1 +
 .../0007_providerschema_additional_lrs.py     | 18 ++++++++++++
 src/providers/models.py                       |  1 +
 src/static/provider_schema.schema.json        | 19 +++++++++++++
 src/xapi/views.py                             | 11 ++++++++
 8 files changed, 79 insertions(+), 3 deletions(-)
 create mode 100644 src/providers/migrations/0007_providerschema_additional_lrs.py

diff --git a/docs/docs/provider_schema.md b/docs/docs/provider_schema.md
index 6c06b6e..b121e2e 100644
--- a/docs/docs/provider_schema.md
+++ b/docs/docs/provider_schema.md
@@ -186,8 +186,31 @@ Essential verbs are verbs that are always collected. The user can not disagree t
 ```
 The configuration of the verbs is identical to that of other verbs within the schema. You can also restrict the verbs to specific verbs. That allows the essential data collection for example for specific objects using the id-based filtering approach. 
 
+### Additional LRS
+Additional LRS can be supplied to forward incoming xAPI statements for the given provider to further LRS, in addition to the global LRS set for the application. Statements are forwarded without further filtering.
+
+```json
+{
+  "id": "h5p-0",
+  "name": "H5P",
+  "description": "Open-source content collaboration framework",
+  "groups": [
+    ...
+  ],
+  "essentialVerbs": [
+    ...
+  ],
+  "additionalLrs": [
+    {
+      "url": "https://other-lrs.example.com/xapi/statements",
+      "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMyNzE5..."
+    }
+  ]
+}
+```
+
 ## Validate Schema 
-It is possible to validate the schema againts the json schema from the repository of the rights engine (`src/static/provider_schema.schema.json`). You can use multiple online tools like https://www.jsonschemavalidator.net/ to validated the schema before uploading it to Polaris. 
+It is possible to validate the schema againts the json schema from the repository of the rights engine (`src/static/provider_schema.schema.json`). You can use multiple online tools like https://www.jsonschemavalidator.net/ to validate the schema before uploading it to Polaris. 
 
 ## Update Schema 
 
@@ -304,7 +327,8 @@ You have the option to retain the existing schema, translate specific properties
       "isDefault": true
     }
   ],
-  "essentialVerbs": []
+  "essentialVerbs": [],
+  "additionalLrs": []
 }
 
 
diff --git a/src/consents/serializers.py b/src/consents/serializers.py
index e6c0749..59886d5 100644
--- a/src/consents/serializers.py
+++ b/src/consents/serializers.py
@@ -15,7 +15,7 @@ class ProviderSchemaSerializer(serializers.ModelSerializer):
             "name": obj.provider.name,
             "description": obj.provider.description,
             "groups": obj.groups,
-            "essential_verbs": obj.essential_verbs,
+            "essential_verbs": obj.essential_verbs
         }
 
     class Meta:
diff --git a/src/consents/views.py b/src/consents/views.py
index 90ccf7a..a2e359e 100644
--- a/src/consents/views.py
+++ b/src/consents/views.py
@@ -261,6 +261,7 @@ class CreateProviderConsentView(APIView):
                     provider=providerModel,
                     groups=provider_schema["groups"],
                     essential_verbs=provider_schema["essentialVerbs"],
+                    additional_lrs=provider_schema["additionalLrs"]
                 )
                 precedingProviderSchema.superseded_by = providerSchemaModel
                 precedingProviderSchema.save()
@@ -275,6 +276,7 @@ class CreateProviderConsentView(APIView):
                 provider=providerModel,
                 groups=provider_schema["groups"],
                 essential_verbs=provider_schema["essentialVerbs"],
+                additional_lrs=provider_schema["additionalLrs"]
             )
 
         return JsonResponse(
diff --git a/src/fixtures/initial_db.json b/src/fixtures/initial_db.json
index 531c54a..abbeecd 100644
--- a/src/fixtures/initial_db.json
+++ b/src/fixtures/initial_db.json
@@ -4511,6 +4511,7 @@
             }
         ],
         "essential_verbs": [],
+        "additional_lrs": [],
         "updated": "2023-03-16T09:02:08.898Z",
         "created": "2023-03-16T09:02:08.898Z"
     }
diff --git a/src/providers/migrations/0007_providerschema_additional_lrs.py b/src/providers/migrations/0007_providerschema_additional_lrs.py
new file mode 100644
index 0000000..1c5fed3
--- /dev/null
+++ b/src/providers/migrations/0007_providerschema_additional_lrs.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2024-11-27 15:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('providers', '0006_analyticstoken_description_analyticstoken_image_path'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='providerschema',
+            name='additional_lrs',
+            field=models.JSONField(default=list),
+        ),
+    ]
diff --git a/src/providers/models.py b/src/providers/models.py
index e14fb36..e344a2b 100644
--- a/src/providers/models.py
+++ b/src/providers/models.py
@@ -22,6 +22,7 @@ class ProviderSchema(models.Model):
     superseded_by = models.ForeignKey("self", null=True, on_delete=models.SET_NULL)
     groups = models.JSONField()
     essential_verbs = models.JSONField()
+    additional_lrs = models.JSONField(default=list)
     updated = models.DateTimeField(auto_now=True)
     created = models.DateTimeField(auto_now_add=True)
 
diff --git a/src/static/provider_schema.schema.json b/src/static/provider_schema.schema.json
index c017ac5..a51b518 100644
--- a/src/static/provider_schema.schema.json
+++ b/src/static/provider_schema.schema.json
@@ -133,6 +133,25 @@
           "required": ["id", "label", "description", "defaultConsent", "objects"]
         }
       ]
+    },
+    "additionalLrs": {
+      "type": "array",
+      "items": [
+        {
+          "type": "object",
+          "properties": {
+            "url": {
+              "type": "string",
+              "description": "URL of the store-endpoint for an additional LRS"
+            },
+            "token": {
+              "type": "string",
+              "description": "token to authenticate with"
+            }
+          },
+          "required": ["url", "token"]
+        }
+      ]
     }
   },
   "required": ["id", "name", "description", "groups", "essentialVerbs"]
diff --git a/src/xapi/views.py b/src/xapi/views.py
index 382224e..18e028a 100644
--- a/src/xapi/views.py
+++ b/src/xapi/views.py
@@ -315,6 +315,17 @@ class CreateStatement(APIView):
             request.data if isinstance(request.data, list) else [request.data]
         )
 
+        # forward to other LRS without validation etc., if given
+        if latest_schema.additional_lrs and isinstance(latest_schema.additional_lrs, list) and len(latest_schema.additional_lrs) > 0:
+            for additional_lrs in latest_schema.additional_lrs:
+                headers = {"Authorization": "Bearer " + additional_lrs["token"]}
+                for stmt in x_api_statements:
+                    res = requests.post(additional_lrs["url"], json=stmt, headers=headers)
+                    if not res and settings.DEBUG:
+                        print("Could not forward to ", additional_lrs["url"], ":", res.reason, "({})".format(res.status_code))
+                    elif res and settings.DEBUG:
+                        print("Forwarded statement to ", additional_lrs["url"], ":", res.reason, "({})".format(res.status_code))
+
         if settings.SHOW_XAPI_STATEMENTS:
            print(x_api_statements)
 
-- 
GitLab