-
Notifications
You must be signed in to change notification settings - Fork 65
Add app scorm_data for mobile sync #48
base: master
Are you sure you want to change the base?
Changes from all commits
25de491
6980cf2
4e8e7d4
435718f
e98c975
6e239ad
af9192f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,10 @@ | ||||||
from django.conf.urls import url | ||||||
|
||||||
from .views import SyncXBlockData | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would use a full path in the import block since it is more explicit instead of using a relative import approach.
Suggested change
|
||||||
|
||||||
|
||||||
urlpatterns = [ | ||||||
url( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An important question is are we going to use that changes with the project before juniper? I expect that we are not, so we can use |
||||||
r'^set_values$', SyncXBlockData.as_view(), name='set_values' | ||||||
), | ||||||
] |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,58 @@ | ||||||||||
# -*- coding: utf-8 -*- | ||||||||||
from __future__ import unicode_literals | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? |
||||||||||
|
||||||||||
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): | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add docstrings with the class description. |
||||||||||
|
||||||||||
def post(self, request, format=None): | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same is here, we need to have the proper description of the |
||||||||||
# TODO: need to take a handler from the request | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why we do not take the handler from the request? |
||||||||||
user = request.user | ||||||||||
handler = 'scorm_set_values' | ||||||||||
response_context = {} | ||||||||||
|
||||||||||
for course_data in request.data.get('courses_data'): | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||||||||||
course_id = course_data.get('course_id') | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we going to work with the None keys in response_context in case 'course_id' is not found in the course_data? |
||||||||||
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)}, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
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)}, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
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('scorm_app: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) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please explain in more details approach that is proposed in L45 - 57, truly saying it is not obvious for me. |
||||||||||
return Response(response_context) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import time | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please reorder imports (alphabetical order is required) |
||
import json | ||
import hashlib | ||
import re | ||
|
@@ -105,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( | ||
|
@@ -207,33 +209,53 @@ 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new function should be described as well. |
||
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': | ||
self.success_status = data.get('value') | ||
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', '') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for that change is also unclear, please clarify. |
||
context.update({"completion_status": self.get_completion_status()}) | ||
return context | ||
|
||
def publish_grade(self): | ||
@XBlock.json_handler | ||
def scorm_get_values(self, data, suffix=''): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New function? What is it stand for? |
||
return self.data_scorm | ||
|
||
@XBlock.json_handler | ||
def scorm_set_values(self, data, suffix=''): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
is_updated = False | ||
if self.data_scorm.get('last_updated_time', 0) < data.get('last_updated_time', 0): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please describe the logic of the updating process. BTW are we sure that request.data contains Additional question how we can check that |
||
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', 0)) | ||
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( | ||
|
@@ -360,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, | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need all files to be included in the package? |
||
zip_safe=False, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need that flag? |
||
install_requires=[ | ||
'XBlock', | ||
], | ||
entry_points={ | ||
'xblock.v1': [ | ||
'scormxblock = scormxblock:ScormXBlock', | ||
] | ||
], | ||
'lms.djangoapp': [ | ||
'scorm_app = scorm_app.apps:MobileScormSyncConfig', | ||
], | ||
'cms.djangoapp': [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A redundant entry point could be omitted. |
||
}, | ||
package_data=package_data("scormxblock", ["static", "public", "translations"]), | ||
license="Apache", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?