From 25de4916a2b5c55045816a8c4b60d3dafb6e24fa Mon Sep 17 00:00:00 2001 From: Sagirov Eugeniy Date: Mon, 21 Dec 2020 13:18:34 +0200 Subject: [PATCH 1/6] EFM-38 Prepare back-end to work with graded SCORM files on mobile apps --- scormxblock/scormxblock.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index a284f325..96cb9a8e 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -1,3 +1,4 @@ +import time import json import hashlib import re @@ -207,13 +208,16 @@ def scorm_get_value(self, data, suffix=''): @XBlock.json_handler def scorm_set_value(self, data, suffix=''): + return self.set_value(data, suffix) + + def set_value(self, data, suffix='', set_last_updated_time=True): context = {'result': 'success'} name = data.get('name') if name in ['cmi.core.lesson_status', 'cmi.completion_status']: self.lesson_status = data.get('value') if self.has_score and data.get('value') in ['completed', 'failed', 'passed']: - self.publish_grade() + self.publish_grade(set_last_updated_time) context.update({"lesson_score": self.format_lesson_score}) elif name == 'cmi.success_status': @@ -221,11 +225,11 @@ def scorm_set_value(self, data, suffix=''): if self.has_score: if self.success_status == 'unknown': self.lesson_score = 0 - self.publish_grade() + self.publish_grade(set_last_updated_time) context.update({"lesson_score": self.format_lesson_score}) elif name in ['cmi.core.score.raw', 'cmi.score.raw'] and self.has_score: self.lesson_score = float(data.get('value', 0))/100.0 - self.publish_grade() + self.publish_grade(set_last_updated_time) context.update({"lesson_score": self.format_lesson_score}) else: self.data_scorm[name] = data.get('value', '') @@ -233,7 +237,21 @@ def scorm_set_value(self, data, suffix=''): context.update({"completion_status": self.get_completion_status()}) return context - def publish_grade(self): + @XBlock.json_handler + def scorm_get_values(self, data, suffix=''): + return self.data_scorm + + @XBlock.json_handler + def scorm_set_values(self, data, suffix=''): + if self.data_scorm.get('last_updated_time', 0) < data.get('last_updated_time'): + for datum in data.get('data'): + self.set_value(datum, suffix, set_last_updated_time=False) + self.data_scorm['last_updated_time'] = int(data.get('last_updated_time')) + return self.data_scorm + + def publish_grade(self, set_last_updated_time=True): + if set_last_updated_time: + self.data_scorm['last_updated_time'] = int(time.time()) if self.lesson_status == 'failed' or (self.version_scorm == 'SCORM_2004' and self.success_status in ['failed', 'unknown']): self.runtime.publish( From 6980cf2325f4bf630c0869f0132fa622fbe3986f Mon Sep 17 00:00:00 2001 From: Sagirov Eugeniy Date: Thu, 24 Dec 2020 18:29:35 +0200 Subject: [PATCH 2/6] changed response from scorm_set_values --- scormxblock/scormxblock.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index 96cb9a8e..e48c9b95 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -231,9 +231,8 @@ def set_value(self, data, suffix='', set_last_updated_time=True): self.lesson_score = float(data.get('value', 0))/100.0 self.publish_grade(set_last_updated_time) context.update({"lesson_score": self.format_lesson_score}) - else: - self.data_scorm[name] = data.get('value', '') + self.data_scorm[name] = data.get('value', '') context.update({"completion_status": self.get_completion_status()}) return context @@ -243,11 +242,15 @@ def scorm_get_values(self, data, suffix=''): @XBlock.json_handler def scorm_set_values(self, data, suffix=''): + is_updated = False if self.data_scorm.get('last_updated_time', 0) < data.get('last_updated_time'): for datum in data.get('data'): self.set_value(datum, suffix, set_last_updated_time=False) self.data_scorm['last_updated_time'] = int(data.get('last_updated_time')) - return self.data_scorm + is_updated = True + context = self.data_scorm + context.update({"is_updated": is_updated}) + return context def publish_grade(self, set_last_updated_time=True): if set_last_updated_time: From 4e8e7d44c90ae150ae0f5127d0b93e98f982d17d Mon Sep 17 00:00:00 2001 From: oksana-slu Date: Fri, 9 Jul 2021 10:15:35 +0300 Subject: [PATCH 3/6] Add mobile sync plugin --- scorm_app/__init__.py | 0 scorm_app/apps.py | 21 ++++++++++++++++ scorm_app/urls.py | 10 ++++++++ scorm_app/views.py | 58 +++++++++++++++++++++++++++++++++++++++++++ setup.py | 9 ++++++- 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 scorm_app/__init__.py create mode 100644 scorm_app/apps.py create mode 100644 scorm_app/urls.py create mode 100644 scorm_app/views.py diff --git a/scorm_app/__init__.py b/scorm_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scorm_app/apps.py b/scorm_app/apps.py new file mode 100644 index 00000000..31ff4bff --- /dev/null +++ b/scorm_app/apps.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + +from openedx.core.djangoapps.plugins.constants import ProjectType, PluginURLs + + +class MobileScormSyncConfig(AppConfig): + name = 'scorm_app' + + plugin_app = { + PluginURLs.CONFIG: { + ProjectType.LMS: { + PluginURLs.NAMESPACE: 'scorm_app', + PluginURLs.APP_NAME: 'scorm_app', + PluginURLs.REGEX: '^mobile_xblock_sync/', + PluginURLs.RELATIVE_PATH: 'urls', + } + } + } diff --git a/scorm_app/urls.py b/scorm_app/urls.py new file mode 100644 index 00000000..2bc9bf49 --- /dev/null +++ b/scorm_app/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url + +from .views import SyncXBlockData + + +urlpatterns = [ + url( + r'^set_values$', SyncXBlockData.as_view(), name='set_values' + ), +] diff --git a/scorm_app/views.py b/scorm_app/views.py new file mode 100644 index 00000000..f2c26b28 --- /dev/null +++ b/scorm_app/views.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import json + +from django.test import RequestFactory +from django.urls import reverse +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from rest_framework import status, views +from rest_framework.response import Response +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError + +from lms.djangoapps.courseware.module_render import _invoke_xblock_handler +from lms.djangoapps.mobile_api.decorators import mobile_course_access, mobile_view + + +@mobile_view() +class SyncXBlockData(views.APIView): + + def post(self, request, format=None): + # TODO: need to take a handler from the request + user = request.user + handler = 'scorm_set_values' + response_context = {} + + for course_data in request.data.get('courses_data'): + course_id = course_data.get('course_id') + response_context[course_id] = {} + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError: + return Response({'error': '{} is not a valid course key'.format(course_id)}, + status=status.HTTP_404_NOT_FOUND) + + with modulestore().bulk_operations(course_key): + try: + course = modulestore().get_course(course_key) + except ItemNotFoundError: + return Response({'error': '{} does not exist in the modulestore'.format(course_id)}, + status=status.HTTP_404_NOT_FOUND) + + for scorm in course_data.get('xblocks_data'): + factory = RequestFactory() + data = json.dumps(scorm) + + scorm_request = factory.post(reverse('mobile_xblock_sync:set_values'), data, + content_type='application/json') + scorm_request.user = user + scorm_request.session = request.session + scorm_request.user.known = True + + usage_id = scorm.get('usage_id') + scorm_response = _invoke_xblock_handler(scorm_request, course_id, usage_id, handler, None, + course=course) + response_context[course_id][usage_id] = json.loads(scorm_response.content) + return Response(response_context) diff --git a/setup.py b/setup.py index bbb15bc1..140272f6 100644 --- a/setup.py +++ b/setup.py @@ -26,14 +26,21 @@ def package_data(pkg, roots): description='scormxblock XBlock', # TODO: write a better description. packages=[ 'scormxblock', + 'scorm_app' ], + include_package_data=True, + zip_safe=False, install_requires=[ 'XBlock', ], entry_points={ 'xblock.v1': [ 'scormxblock = scormxblock:ScormXBlock', - ] + ], + 'lms.djangoapp': [ + 'scorm_app = scorm_app.apps:MobileScormSyncConfig', + ], + 'cms.djangoapp': [], }, package_data=package_data("scormxblock", ["static", "public", "translations"]), license="Apache", From e98c97581344bd5a6d9b76980224887446c7b945 Mon Sep 17 00:00:00 2001 From: Oksana Slusarenko Date: Mon, 2 Aug 2021 15:33:59 +0300 Subject: [PATCH 4/6] fix: UNESCO-41 NoReverseMatch: mobile_xblock_sync (#45) --- scorm_app/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scorm_app/views.py b/scorm_app/views.py index f2c26b28..4131f752 100644 --- a/scorm_app/views.py +++ b/scorm_app/views.py @@ -45,7 +45,7 @@ def post(self, request, format=None): factory = RequestFactory() data = json.dumps(scorm) - scorm_request = factory.post(reverse('mobile_xblock_sync:set_values'), data, + scorm_request = factory.post(reverse('scorm_app:set_values'), data, content_type='application/json') scorm_request.user = user scorm_request.session = request.session From 6e239ade8ffa0f5ffa5e64fb615bbd3ab7a7d773 Mon Sep 17 00:00:00 2001 From: Oksana Slusarenko Date: Mon, 2 Aug 2021 15:38:14 +0300 Subject: [PATCH 5/6] feat: [UNESCO-41] Add flag multi_device and root_url for download data (#46) --- scormxblock/scormxblock.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index e48c9b95..6350a658 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -106,6 +106,7 @@ def resource_string(self, path): data = pkg_resources.resource_string(__name__, path) return data.decode("utf8") + @XBlock.supports("multi_device") def student_view(self, context=None): context_html = self.get_context_student() template = loader.render_django_template( @@ -381,9 +382,14 @@ def student_view_data(self): Make sure to include `student_view_data=scormxblock` to URL params in the request. """ if self.scorm_file and self.scorm_file_meta: + + scorm_data = default_storage.url(self._file_storage_path()) + if not scorm_data.startswith('http'): + scorm_data = '{}{}'.format(settings.LMS_ROOT_URL, scorm_data) + return { 'last_modified': self.scorm_file_meta.get('last_updated', ''), - 'scorm_data': default_storage.url(self._file_storage_path()), + 'scorm_data': scorm_data, 'size': self.scorm_file_meta.get('size', 0), 'index_page': self.path_index_page, } From af9192f0609091057936219d77872f43b751850e Mon Sep 17 00:00:00 2001 From: Oksana Slusarenko Date: Thu, 5 Aug 2021 11:59:10 +0300 Subject: [PATCH 6/6] fix: [UNESCO-41] TypeError: '<' not supported between instances of 'int' and 'NoneType' (#47) --- scormxblock/scormxblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index 6350a658..7757c0db 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -244,10 +244,10 @@ def scorm_get_values(self, data, suffix=''): @XBlock.json_handler def scorm_set_values(self, data, suffix=''): is_updated = False - if self.data_scorm.get('last_updated_time', 0) < data.get('last_updated_time'): + if self.data_scorm.get('last_updated_time', 0) < data.get('last_updated_time', 0): for datum in data.get('data'): self.set_value(datum, suffix, set_last_updated_time=False) - self.data_scorm['last_updated_time'] = int(data.get('last_updated_time')) + self.data_scorm['last_updated_time'] = int(data.get('last_updated_time', 0)) is_updated = True context = self.data_scorm context.update({"is_updated": is_updated})