Skip to content

Commit

Permalink
Custom storage for SCORM xblock
Browse files Browse the repository at this point in the history
  • Loading branch information
nauman4386 committed May 25, 2023
1 parent a47ef1c commit 65865a4
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 4 deletions.
21 changes: 19 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,29 @@ By default, static assets are stored in the default Django storage backend. To o
"STORAGE_FUNC": scorm_storage,
}

This should be added both to the LMS and the CMS settings. Instead of a function, a string that points to an importable module may be passed::
Configuration for SCORM XBlock for AWS S3 storage use case
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to configure the SCORM XBlock for AWS S3 storage use case, you'll need to modify the `XBLOCK_SETTINGS` in both the `lms/envs/private.py` and `cms/envs/private.py` files.

Add the following lines to these files::

# XBlock settings for ScormXBlock
XBLOCK_SETTINGS["ScormXBlock"] = {
"STORAGE_FUNC": "my.custom.storage.module.get_scorm_storage_function",
"STORAGE_FUNC": "openedxscorm.storage.s3",
"SCORM_S3_BUCKET_NAME": "<your-bucket-name>",
"SCORM_S3_QUERY_AUTH": True,
"SCORM_S3_EXPIRES_IN": 604800
}

This configuration is specifically for when using an S3 bucket to store SCORM assets.

The STORAGE_FUNC is set to the s3 storage module in the SCORM XBlock.
SCORM_S3_BUCKET_NAME should be replaced with your specific S3 bucket name. If you do not set SCORM_S3_BUCKET_NAME, you should have a bucket named ScormS3Bucket which you have to newly-created in your AWS account.
SCORM_S3_QUERY_AUTH is a boolean flag that indicates whether or not to use query string authentication for your S3 URLs. If your bucket is public, you should set this value to False. If it is private, no need to set it.
SCORM_S3_EXPIRES_IN sets the time duration (in seconds) for the presigned URLs to stay valid. The default value here is 604800 which corresponds to one week. If this is not set, the default value will be used.
Once you've made these changes, save both files and restart your LMS and Studio instances for the changes to take effect.

Development
-----------

Expand Down
31 changes: 30 additions & 1 deletion openedxscorm/scormxblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import re
import xml.etree.ElementTree as ET
import zipfile
import requests
import mimetypes

from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db.models import Q
from django.template import Context, Template
from django.utils import timezone
from django.utils.module_loading import import_string
from urllib.parse import urlparse
from webob import Response
import pkg_resources
from six import string_types
Expand Down Expand Up @@ -61,7 +64,6 @@ class ScormXBlock(XBlock, CompletableXBlockMixin):
Note that neither the folder the folder nor the package file are deleted when the
xblock is removed.
By default, static assets are stored in the default Django storage backend. To
override this behaviour, you should define a custom storage function. This
function must take the xblock instance as its first and only argument. For instance,
Expand Down Expand Up @@ -202,6 +204,33 @@ def student_view(self, context=None):
)
return frag

@XBlock.handler
def assets_proxy(self, request, _suffix):
"""
Proxy view for serving assets. It receives a request with the path to the asset to serve, generates a pre-signed
URL to access the content in the AWS S3 bucket, and returns a redirect response to the pre-signed URL.
Parameters:
----------
request : django.http.request.HttpRequest
HTTP request object containing the path to the asset to serve.
_suffix : str
The part of the URL after 'assets_proxy/', i.e., the path to the asset to serve.
Returns:
-------
Response object containing the content of the requested file with the appropriate content type.
"""
path = urlparse(_suffix).path
file_name = os.path.basename(path)
signed_url = self.storage.url(path)
file_type, _ = mimetypes.guess_type(file_name)
file_content = requests.get(signed_url).content

return Response(
file_content, content_type=file_type
)

def studio_view(self, context=None):
# Note that we cannot use xblockutils's StudioEditableXBlockMixin because we
# need to support package file uploads.
Expand Down
63 changes: 63 additions & 0 deletions openedxscorm/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Storage backend for scorm metadata export.
"""
import os

from django.core.files.storage import get_storage_class
from storages.backends.s3boto3 import S3Boto3Storage


class S3ScormStorage(S3Boto3Storage):
"""
S3 backend for scorm metadata export
"""
def __init__(self, xblock, bucket, querystring_auth, querystring_expire):
self.xblock = xblock
super().__init__(bucket=bucket, querystring_auth=querystring_auth,
querystring_expire=querystring_expire)

def url(self, name):
"""
Override url method of S3Boto3Storage
"""
if not self.querystring_auth:
return super().url(name)

if name.startswith(self.xblock.extract_folder_path):
handler_url = self.xblock.runtime.handler_url(self.xblock, 'assets_proxy')

# remove trailing '?' if it's present
if handler_url.endswith('?'):
handler_url = handler_url[:-1]
# add '/' if not present at the end
elif not handler_url.endswith('/'):
handler_url += '/'

# construct the URL for proxy function
return f'{handler_url}{self.xblock.index_page_path}'

return super().url(os.path.join(self.xblock.extract_folder_path, name))


def s3(xblock):
"""
Creates and returns an instance of the S3ScormStorage class.
This function takes an xblock instance as its argument and returns an instance
of the S3ScormStorage class. The S3ScormStorage class is defined in the
'openedxscorm.storage' module and provides storage functionality specific to
SCORM XBlock.
Args:
xblock (XBlock): An instance of the SCORM XBlock.
Returns:
S3ScormStorage: An instance of the S3ScormStorage class.
"""
bucket = xblock.xblock_settings.get('SCORM_S3_BUCKET_NAME', "ScormS3Bucket")
querystring_auth = xblock.xblock_settings.get('SCORM_S3_QUERY_AUTH', True)
querystring_expire = xblock.xblock_settings.get('SCORM_S3_EXPIRES_IN', 604800)
storage_class = get_storage_class('openedxscorm.storage.S3ScormStorage')
return storage_class(xblock=xblock, bucket=bucket,
querystring_auth=querystring_auth,
querystring_expire=querystring_expire)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def package_data(pkg, roots):

setup(
name="openedx-scorm-xblock",
version="15.0.0",
version="15.0.1",
description="Scorm XBlock for Open edX",
long_description=readme,
long_description_content_type="text/x-rst",
Expand Down

0 comments on commit 65865a4

Please sign in to comment.