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/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index a284f325..e48c9b95 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,19 +225,36 @@ 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', '') + self.data_scorm[name] = data.get('value', '') 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=''): + 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')) + 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: + 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( 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",