From 9fddeffd3ddc36f9fbc137329c785e5beec32e78 Mon Sep 17 00:00:00 2001 From: nauman4386 Date: Sat, 13 May 2023 22:59:12 +0500 Subject: [PATCH] Custom storage for SCORM xblock --- openedxscorm/scormxblock.py | 65 ++++++++++++++++++++++++++++++------- openedxscorm/storage.py | 37 +++++++++++++++++++++ setup.py | 2 +- 3 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 openedxscorm/storage.py diff --git a/openedxscorm/scormxblock.py b/openedxscorm/scormxblock.py index ea9cc77..5e43e53 100644 --- a/openedxscorm/scormxblock.py +++ b/openedxscorm/scormxblock.py @@ -5,6 +5,8 @@ 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 @@ -61,7 +63,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, @@ -202,6 +203,35 @@ def student_view(self, context=None): ) return frag + @XBlock.handler + def scorm_view(self, request, _suffix): + """ + View for serving SCORM content. It receives a request with the path to the SCORM content to serve, generates a pre-signed + URL to access the content in the AWS S3 bucket, retrieves the file content and returns it with the appropriate content + type. + + Parameters: + ---------- + request : django.http.request.HttpRequest + HTTP request object containing the path to the SCORM content to serve. + _suffix : str + Unused parameter. + + Returns: + ------- + Response object containing the content of the requested file with the appropriate content type. + """ + path = request.url.split('scorm_view/')[-1] + path = re.sub(r"(\?.*|:[\d:]*$)", "", path) + file_name = os.path.basename(path) + signed_url = self.storage.url(os.path.join(self.extract_folder_path, path)) + file_content = requests.get(signed_url).content + file_type, _ = mimetypes.guess_type(file_name) + + 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. @@ -326,20 +356,33 @@ def extract_package(self, package_file): dest_path, ContentFile(scorm_zipfile.read(zipinfo.filename)), ) - + @property def index_page_url(self): if not self.package_meta or not self.index_page_path: return "" - folder = self.extract_folder_path - if self.storage.exists( - os.path.join(self.extract_folder_base_path, self.index_page_path) - ): - # For backward-compatibility, we must handle the case when the xblock data - # is stored in the base folder. - folder = self.extract_folder_base_path - logger.warning("Serving SCORM content from old-style path: %s", folder) - return self.storage.url(os.path.join(folder, self.index_page_path)) + + querystring_auth_setting = self.xblock_settings.get('SCORM_S3_QUERY_AUTH') + if querystring_auth_setting is False: + folder = self.extract_folder_path + if self.storage.exists( + os.path.join(self.extract_folder_base_path, self.index_page_path) + ): + # For backward-compatibility, we must handle the case when the xblock data + # is stored in the base folder. + folder = self.extract_folder_base_path + logger.warning("Serving SCORM content from old-style path: %s", folder) + return self.storage.url(os.path.join(folder, self.index_page_path)) + else: + url = self.runtime.handler_url(self, 'scorm_view') + if url.endswith('?'): + url = url.split('?')[0] + self.index_page_path + elif not url.endswith('/'): + url = url + '/' + self.index_page_path + else: + url = self.runtime.handler_url(self, 'scorm_view') + self.index_page_path + + return url @property def extract_folder_path(self): diff --git a/openedxscorm/storage.py b/openedxscorm/storage.py new file mode 100644 index 0000000..ecea680 --- /dev/null +++ b/openedxscorm/storage.py @@ -0,0 +1,37 @@ +""" +Storage backend for scorm metadata export. +""" +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, bucket, querystring_auth, querystring_expire): + super().__init__(bucket=bucket, querystring_auth=querystring_auth, + querystring_expire=querystring_expire) + + +def scorm_storage(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', None) + querystring_auth = xblock.xblock_settings.get('SCORM_S3_QUERY_AUTH', None) + querystring_expire = xblock.xblock_settings.get('SCORM_S3_EXPIRES_IN', 604800) + storage_class = get_storage_class('openedxscorm.storage.S3ScormStorage') + return storage_class(bucket=bucket, querystring_auth=querystring_auth, + querystring_expire=querystring_expire) diff --git a/setup.py b/setup.py index 987531b..4c32c9d 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ def package_data(pkg, roots): setup( name="openedx-scorm-xblock", - version="15.0.0", + version="15.1.0", description="Scorm XBlock for Open edX", long_description=readme, long_description_content_type="text/x-rst",