Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose xAPI forwarding endpoint #88

Merged
merged 23 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ When testing use root to allow the creation of a test database

`CERT_VOLUME` - The path to the certificate (on the host machine) to use when connecting to AWS

`XAPI_ALLOW_ANON` - If `true` (default) will allow forwarding of anonymous xAPI Statements. Not compatible with `XAPI_USE_JWT`.

`XAPI_ANON_MBOX` - The mbox email value to use for anonymous xAPI actors if `XAPI_ALLOW_ANON` is enabled. Defaults to `[email protected]`.

`XAPI_USE_JWT` - If this variable is set, attempt to use the value of a JWT auth token to derive the xAPI actor account. If not set the actor will be identified by mbox email. Not compatible with `XAPI_ALLOW_ANON`.

`XAPI_ACTOR_ACCOUNT_HOMEPAGE` - Set the `$.actor.account.homePage` field on xAPI Statements. Only used when `XAPI_USE_JWT` is `true`.

`XAPI_ACTOR_ACCOUNT_NAME_JWT_FIELDS` - A comma-separated list of fields to check in the JWT for the `$.actor.account.name` field on xAPI Statements. The first non-empty string found will be chosen. Defaults to `activecac,preferred_username`. Only used when `XAPI_USE_JWT` is `true`.


# Installation

Expand Down Expand Up @@ -87,8 +97,14 @@ When testing use root to allow the creation of a test database

`Target xse index`: Index of data to use on XSE instance.

`LRS Endpoint`: xAPI LRS Endpoint to send statements to if desired.

`LRS Username`: xAPI LRS Basic Auth username.

`LRS Password`: xAPI LRS Basic Auth password.


3. `Add xdsui configuration`: Configure Experience Discovery Service - User Interface (XDS-UI):
3. `Add xdsui configuration`: Configure Experience Discovery Service - User Interface (XDS-UI):

`Search results per page`: Number of results that should be displayed on a search page on the UI.

Expand Down
6 changes: 5 additions & 1 deletion app/configurations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ class XDSConfigurationAdmin(admin.ModelAdmin):
fieldsets = (
('XDS Settings', {'fields': ('default_user_group',)}),
('XIS Settings', {'fields': ('target_xis_metadata_api',)}),
('XSE Settings', {'fields': ('target_xse_host', 'target_xse_index',)}))
('XSE Settings', {'fields': ('target_xse_host', 'target_xse_index',)}),
('LRS Settings', {'fields': ('lrs_endpoint',
'lrs_username',
'lrs_password')})
)


@admin.register(XDSUIConfiguration)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.18 on 2025-01-16 20:38

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('configurations', '0012_courseinformationmapping_course_subject'),
]

operations = [
migrations.AddField(
model_name='xdsconfiguration',
name='lrs_endpoint',
field=models.CharField(blank=True, help_text='Enter the xAPI LRS Endpoint to send data to', max_length=200, null=True),
),
migrations.AddField(
model_name='xdsconfiguration',
name='lrs_password',
field=models.CharField(blank=True, help_text='Enter the xAPI LRS HTTP Basic Auth password', max_length=200, null=True),
),
migrations.AddField(
model_name='xdsconfiguration',
name='lrs_username',
field=models.CharField(blank=True, help_text='Enter the xAPI LRS HTTP Basic Auth username', max_length=200, null=True),
),
]
18 changes: 18 additions & 0 deletions app/configurations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ class XDSConfiguration(TimeStampedModel):
blank=True,
null=True
)
lrs_endpoint = models.CharField(
max_length=200,
help_text='Enter the xAPI LRS Endpoint to send data to',
blank=True,
null=True
)
lrs_username = models.CharField(
max_length=200,
help_text='Enter the xAPI LRS HTTP Basic Auth username',
blank=True,
null=True
)
lrs_password = models.CharField(
max_length=200,
help_text='Enter the xAPI LRS HTTP Basic Auth password',
blank=True,
null=True
)

def get_absolute_url(self):
""" URL for displaying individual model records."""
Expand Down
49 changes: 37 additions & 12 deletions app/openlxp_xds_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,41 @@
]
}

EMAIL_BACKEND = 'django_ses.SESBackend'


# Django-notifications package settings
DJANGO_NOTIFICATIONS_CONFIG = {
'USE_JSONFIELD': True,
}

# when notifications should be automatically deleted, should be days or greater
NOTIFICATIONS_EXPIRE_AFTER = datetime.timedelta(days=30)

DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880

# xAPI Statement Forwarding Settings

# whether to allow anonymous xAPI statement forwarding
XAPI_ALLOW_ANON = os.getenv('XAPI_ALLOW_ANON', 'true').lower() == 'true'

# mbox email to use for the anonymous user
XAPI_ANON_MBOX = os.getenv('XAPI_ANON_MBOX', '[email protected]')

# toggle setting actor from JWT
XAPI_USE_JWT = os.getenv('XAPI_USE_JWT', 'false').lower() == 'true'

# Set $.actor.account.homePage on statements.
XAPI_ACTOR_ACCOUNT_HOMEPAGE = os.environ.get('XAPI_ACTOR_ACCOUNT_HOMEPAGE',
'https://example.com')
# Define fields from JWT to use for $.actor.account.name on statements in
# descending order of preference.
XAPI_ACTOR_ACCOUNT_NAME_JWT_FIELDS = [
field.strip()
for field in os.environ.get('XAPI_ACTOR_ACCOUNT_NAME_JWT_FIELDS', 'activecac,preferred_username').split(',')
]


# Accepts regex arguments
OPEN_ENDPOINTS = [
"/api/auth/register",
Expand All @@ -279,15 +314,5 @@
"/api/spotlight-courses",
]

EMAIL_BACKEND = 'django_ses.SESBackend'


# Django-notifications package settings
DJANGO_NOTIFICATIONS_CONFIG = {
'USE_JSONFIELD': True,
}

# when notifications should be automatically deleted, should be days or greater
NOTIFICATIONS_EXPIRE_AFTER = datetime.timedelta(days=30)

DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880
if XAPI_ALLOW_ANON and not XAPI_USE_JWT:
OPEN_ENDPOINTS.append("/api/statements")
5 changes: 5 additions & 0 deletions app/xds_api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@

class XdsApiConfig(AppConfig):
name = 'xds_api'

def ready(self):
import xds_api.signals
xds_api.signals.add_permissions
return super().ready()
56 changes: 56 additions & 0 deletions app/xds_api/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import logging

from django.db.models.signals import post_migrate
from django.dispatch import receiver

logger = logging.getLogger('dict_config_logger')

GROUPS = ['System Operator', 'Experience Owner', 'Experience Manager',
'Experience Facilitator', 'Experience Participant']
MODELS = ['statement forward', ]
PERMISSIONS = ['view', 'add', 'change', 'delete']


@receiver(post_migrate)
def add_permissions(sender, verbosity, stdout, using, apps, **kwargs):
Group = apps.get_model('auth', 'Group')
Perm = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')

for custom in MODELS:
ContentType.objects.get_or_create(
app_label='xds_api',
model=custom.replace(" ", "")
)

for group in GROUPS:
new_group, created = Group.objects.get_or_create(name=group)

for model in MODELS:
for permission in PERMISSIONS:
name = 'Can {} {}'.format(permission, model)
if verbosity > 1:
stdout.write(msg="Creating {}".format(name))
stdout.flush()

model_comb = model
value = model_comb.replace(" ", "")
codename = '{}_{}'.format(permission, value)

try:
content_type = \
ContentType.objects.using(using).get(model=value)
model_add_perm, created = \
Perm.objects.using(using).get_or_create(
codename=codename,
name=name,
content_type=content_type)
if not new_group.permissions.contains(model_add_perm):
new_group.permissions.add(model_add_perm)
except Perm.DoesNotExist:
if verbosity > 0:
stdout.write(
msg="Permission not found with name '{}'.".
format(name))
stdout.flush()
continue
7 changes: 6 additions & 1 deletion app/xds_api/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ def setUp(self):
self.list_1.experiences.add(self.course_1)
self.list_2.experiences.add(self.course_1)

self.config = XDSConfiguration(target_xis_metadata_api="test").save()
self.config = XDSConfiguration(
target_xis_metadata_api="test",
lrs_endpoint="http://lrs.example.com/xapi",
lrs_username="username",
lrs_password="testpass"
).save()

return super().setUp()

Expand Down
Loading
Loading