From 019150041ce9e6f7930d631f7ced2843f2fdb032 Mon Sep 17 00:00:00 2001 From: Chiruzzi Marco Date: Tue, 27 Sep 2022 17:09:25 +0200 Subject: [PATCH 1/4] Attempt to fix the grading and make the XBlock compatible with the Completion API --- .github/workflows/python-app.yml | 2 +- .../abstract_scorm_xblock/scormxblock.py | 97 ++++++++++----- .../abstract_scorm_xblock/tests.py | 12 +- derex_project/derex.config.yaml | 2 +- requirements_dev.in | 2 +- requirements_dev.txt | 114 +++++++++--------- 6 files changed, 136 insertions(+), 93 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 386f754..aafba3d 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -35,7 +35,7 @@ jobs: derex reset-rabbitmq # We can't use `derex create-bucket` because github CI doesn't allocate a TTY docker run --rm --network derex --entrypoint /bin/sh minio/mc -c 'mc config host add local http://minio:80 minio_derex "ICDTE0ZnlbIR7r6/qE81nkF7Kshc2gXYv6fJR4I/HKPeTbxEeB3nxC85Ne6C844hEaaC2+KHBRIOzGou9leulZ7t" --api s3v4; set -ex; mc mb --ignore-existing local/scorm; mc policy set download local/scorm/profile-images' - derex build final + derex build project - name: Run tests run: | make coverage diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py b/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py index 8a59b29..739d8a4 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py +++ b/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py @@ -16,6 +16,7 @@ from xblock.core import XBlock from xblock.fields import Scope, String, Float, Boolean, Dict, Integer from xblock.fragment import Fragment +from xblock.completable import CompletableXBlockMixin from .utils import gettext as _ from .utils import resource_string, render_template @@ -26,7 +27,7 @@ logger = logging.getLogger(__name__) -class AbstractScormXBlock(XBlock): +class AbstractScormXBlock(XBlock, CompletableXBlockMixin): display_name = String( display_name=_("Display Name"), help=_("Display name for this module"), @@ -123,7 +124,7 @@ def student_view(self, context={}): template = render_template( "static/html/scormxblock.html", - {"completion_status": self._get_completion_status(), "scorm_xblock": self}, + {"completion_status": self.get_lesson_status(), "scorm_xblock": self}, ) fragment = Fragment(template) fragment.add_css(resource_string("static/css/scormxblock.css")) @@ -131,7 +132,7 @@ def student_view(self, context={}): js_settings = { "scorm_version": self._scorm_version, "scorm_url": self._scorm_url, - "completion_status": self._get_completion_status(), + "completion_status": self.get_lesson_status(), "scorm_xblock": { "display_name": self.display_name, "width": self.width, @@ -217,6 +218,10 @@ def studio_submit(self, request, suffix=""): status=200, ) + def get_current_user_attributes(self, attribute): + user = self.runtime.service(self, "user").get_current_user() + return user.opt_attrs.get(attribute) + @XBlock.json_handler def scorm_get_value(self, data, suffix=""): name = data.get("name") @@ -226,6 +231,10 @@ def scorm_get_value(self, data, suffix=""): return {"value": self._success_status} elif name in ["cmi.core.score.raw", "cmi.score.raw"]: return {"value": self.lesson_score * 100} + elif name in ["cmi.core.student_id", "cmi.learner_id"]: + return {"value": self.get_current_user_attr("edx-platform.user_id")} + elif name in ["cmi.core.student_name", "cmi.learner_name"]: + return {"value": self.get_current_user_attr("edx-platform.username")} else: return {"value": self._scorm_data.get(name, "")} @@ -233,33 +242,63 @@ def scorm_get_value(self, data, suffix=""): def scorm_set_value(self, data, suffix=""): payload = {"result": "success"} name = data.get("name") - + value = data.get("value") + lesson_score = None + lesson_status = None + success_status = None + completion_status = None + completion_percent = None + + self._scorm_data[name] = value 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() - payload.update({"lesson_score": self.lesson_score}) + lesson_status = value + if lesson_status in ["passed", "failed"]: + success_status = lesson_status + elif lesson_status in ["completed", "incomplete"]: + completion_status = lesson_status 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() - payload.update({"lesson_score": self.lesson_score}) + success_status = value + elif name == "cmi.completion_status": + completion_status = value elif name in ["cmi.core.score.raw", "cmi.score.raw"] and self.has_score: - self.lesson_score = int(data.get("value", 0)) / 100.0 - self._publish_grade() - payload.update({"lesson_score": self.lesson_score}) - else: - self._scorm_data[name] = data.get("value", "") + lesson_score = float(value) / 100.0 + elif name == "cmi.progress_measure": + completion_percent = float(value) - payload.update({"completion_status": self._get_completion_status()}) + payload = {"result": "success"} + if lesson_score is not None: + self.lesson_score = lesson_score + if success_status in ["failed", "unknown"]: + lesson_score = 0 + else: + lesson_score = lesson_score * self.weight + payload.update({"lesson_score": lesson_score}) + if lesson_status: + self._lesson_status = lesson_status + payload.update({"completion_status": lesson_status}) + if completion_percent is not None: + self.emit_completion(completion_percent) + if completion_status: + self.completion_status = completion_status + payload.update({"completion_status": completion_status}) + if success_status: + self._success_status = success_status + if completion_status == "completed": + self.emit_completion(1) + if success_status or completion_status == "completed": + if self.has_score: + self._publish_grade() return payload + def set_score(self, score): + """ + Utility method used to rescore a problem. + """ + if self.has_score: + self.lesson_score = score.raw_earned + self._publish_grade() + self.emit_completion(1) + def _publish_grade(self): if self._lesson_status == "failed" or ( self.scorm_file @@ -272,15 +311,17 @@ def _publish_grade(self): self, "grade", {"value": self.lesson_score, "max_value": self.weight} ) - def _get_completion_status(self): - completion_status = self._lesson_status + def get_lesson_status(self): + lesson_status = self._lesson_status if ( self.scorm_file and ScormVersions(self._scorm_version) > ScormVersions["SCORM_12"] and self._success_status != "unknown" ): - completion_status = self._success_status - return completion_status + lesson_status = self._success_status + if not lesson_status: + lesson_status = "unknown" + return lesson_status def _read_scorm_manifest(self, scorm_path): manifest_path = os.path.join(scorm_path, "imsmanifest.xml") diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/tests.py b/abstract_scorm_xblock/abstract_scorm_xblock/tests.py index 375cf27..3402530 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/tests.py +++ b/abstract_scorm_xblock/abstract_scorm_xblock/tests.py @@ -117,7 +117,7 @@ def test_set_status(self, value, _publish_grade, _get_completion_status): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( - mock.Mock(method="POST", body=json.dumps(value)) + mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) _publish_grade.assert_called_once_with() @@ -149,7 +149,7 @@ def test_set_lesson_score(self, value, _get_completion_status): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( - mock.Mock(method="POST", body=json.dumps(value)) + mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) _get_completion_status.assert_called_once_with() @@ -178,7 +178,7 @@ def test_set_other_scorm_values(self, value, _get_completion_status): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( - mock.Mock(method="POST", body=json.dumps(value)) + mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) _get_completion_status.assert_called_once() @@ -199,7 +199,7 @@ def test_scorm_get_status(self, value): xblock = self.make_one(_lesson_status="status", _success_status="status") response = xblock.scorm_get_value( - mock.Mock(method="POST", body=json.dumps(value)) + mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) self.assertEqual(response.json, {"value": "status"}) @@ -212,7 +212,7 @@ def test_scorm_get_lesson_score(self, value): xblock = self.make_one(lesson_score=0.2) response = xblock.scorm_get_value( - mock.Mock(method="POST", body=json.dumps(value)) + mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) self.assertEqual(response.json, {"value": 20}) @@ -232,7 +232,7 @@ def test_get_other_scorm_values(self, value): ) response = xblock.scorm_get_value( - mock.Mock(method="POST", body=json.dumps(value)) + mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) self.assertEqual(response.json, {"value": xblock._scorm_data[value["name"]]}) diff --git a/derex_project/derex.config.yaml b/derex_project/derex.config.yaml index 32fc292..8899d15 100644 --- a/derex_project/derex.config.yaml +++ b/derex_project/derex.config.yaml @@ -1,2 +1,2 @@ project_name: scorm -openedx_version: ironwood +openedx_version: juniper diff --git a/requirements_dev.in b/requirements_dev.in index 41c6825..f9a0484 100644 --- a/requirements_dev.in +++ b/requirements_dev.in @@ -1,2 +1,2 @@ -https://github.com/Abstract-Tech/derex.runner/tarball/v0.0.3#egg=derex.runner +https://github.com/Abstract-Tech/derex.runner/tarball/v0.3.4.dev5#egg=derex.runner pre-commit diff --git a/requirements_dev.txt b/requirements_dev.txt index 9e08ec8..7500627 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,44 +6,38 @@ # appdirs==1.4.4 # via derex-runner -attrs==19.3.0 +attrs==22.2.0 # via jsonschema -bcrypt==3.1.7 +bcrypt==4.0.1 # via paramiko -cached-property==1.5.1 - # via docker-compose -certifi==2020.6.20 +certifi==2022.12.7 # via requests -cffi==1.14.0 +cffi==1.15.1 # via - # bcrypt # cryptography # pynacl -cfgv==3.1.0 +cfgv==3.3.1 # via pre-commit -chardet==3.0.4 +charset-normalizer==3.0.1 # via requests -click==7.1.2 +click==8.1.3 # via # click-plugins # derex-runner + # typer click-plugins==1.1.1 # via derex-runner -colorama==0.4.3 - # via rich -commonmark==0.9.1 - # via rich -cryptography==2.9.2 +cryptography==39.0.1 # via paramiko -derex.runner @ https://github.com/Abstract-Tech/derex.runner/tarball/v0.0.3 +derex.runner @ https://github.com/Abstract-Tech/derex.runner/tarball/v0.3.4.dev5 # via -r requirements_dev.in distlib==0.3.6 # via virtualenv -distro==1.5.0 +distro==1.8.0 # via docker-compose -docker[ssh]==4.2.2 +docker[ssh]==6.0.1 # via docker-compose -docker-compose==1.26.1 +docker-compose==1.29.2 # via derex-runner dockerpty==0.4.1 # via docker-compose @@ -51,81 +45,89 @@ docopt==0.6.2 # via docker-compose filelock==3.9.0 # via virtualenv -identify==1.4.20 +identify==2.5.18 # via pre-commit -idna==2.10 +idna==3.4 # via requests -importlib-metadata==1.7.0 +importlib-metadata==6.0.0 # via derex-runner -jinja2==2.11.2 +jinja2==3.1.2 # via derex-runner jsonschema==3.2.0 # via docker-compose -markupsafe==1.1.1 +markdown-it-py==2.1.0 + # via rich +markupsafe==2.1.2 # via jinja2 -nodeenv==1.4.0 +mdurl==0.1.2 + # via markdown-it-py +nodeenv==1.7.0 # via pre-commit -paramiko==2.7.1 +packaging==23.0 + # via docker +paramiko==3.0.0 # via docker platformdirs==3.0.0 # via virtualenv -pluggy==0.13.1 +pluggy==1.0.0 # via derex-runner -pprintpp==0.4.0 - # via rich pre-commit==3.0.4 # via -r requirements_dev.in -pycparser==2.20 +pycparser==2.21 # via cffi -pygments==2.6.1 +pydantic==1.10.5 + # via python-on-whales +pygments==2.14.0 # via rich -pymongo==3.10.1 +pymongo==3.13.0 # via derex-runner -pymysql==0.9.3 +pymysql==1.0.2 # via derex-runner -pynacl==1.4.0 +pynacl==1.5.0 # via paramiko -pyrsistent==0.16.0 +pyrsistent==0.19.3 # via jsonschema -python-dotenv==0.13.0 +python-dotenv==0.21.1 # via docker-compose -pyyaml==5.3.1 +python-on-whales==0.58.0 + # via derex-runner +pyyaml==5.4.1 # via - # derex-runner # docker-compose # pre-commit -requests==2.24.0 +requests==2.28.2 # via # docker # docker-compose -rich==3.0.1 + # python-on-whales +rich==13.3.1 # via derex-runner -six==1.15.0 +six==1.16.0 # via - # bcrypt - # cryptography - # docker - # docker-compose # dockerpty # jsonschema - # pynacl - # pyrsistent # websocket-client -tabulate==0.8.7 - # via derex-runner -texttable==1.6.2 +texttable==1.6.7 # via docker-compose -typing-extensions==3.7.4.2 - # via rich -urllib3==1.25.9 - # via requests +tqdm==4.64.1 + # via python-on-whales +typer==0.7.0 + # via python-on-whales +typing-extensions==4.5.0 + # via + # pydantic + # python-on-whales +urllib3==1.26.14 + # via + # docker + # requests virtualenv==20.19.0 # via pre-commit -websocket-client==0.57.0 +websocket-client==0.59.0 # via # docker # docker-compose -zipp==3.1.0 +zipp==3.14.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: From 4aba512fd5e0c07634cfe12177d87c7631f7d9f4 Mon Sep 17 00:00:00 2001 From: Chiruzzi Marco Date: Tue, 25 Oct 2022 18:58:42 +0200 Subject: [PATCH 2/4] Fix lesson score display and resume function --- .../abstract_scorm_xblock/scormxblock.py | 16 +++++++++++++--- .../static/html/scormxblock.html | 2 +- .../static/js/src/scormxblock.js | 16 +++++++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py b/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py index 739d8a4..3cf91d1 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py +++ b/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py @@ -111,6 +111,12 @@ class AbstractScormXBlock(XBlock, CompletableXBlockMixin): _success_status = String(scope=Scope.user_state, default="unknown") _scorm_data = Dict(scope=Scope.user_state, default={}) + @property + def lesson_score_display(self): + if self.has_score and self.lesson_score == self.weight: + return int(self.weight) + return round(self.lesson_score, 2) + def student_view(self, context={}): # TODO: We should be able to display an error message # instead of trying to render an inexistent or problematic @@ -132,6 +138,7 @@ def student_view(self, context={}): js_settings = { "scorm_version": self._scorm_version, "scorm_url": self._scorm_url, + "scorm_data": self._scorm_data, "completion_status": self.get_lesson_status(), "scorm_xblock": { "display_name": self.display_name, @@ -229,7 +236,7 @@ def scorm_get_value(self, data, suffix=""): return {"value": self._lesson_status} elif name == "cmi.success_status": return {"value": self._success_status} - elif name in ["cmi.core.score.raw", "cmi.score.raw"]: + elif name in ["cmi.core.score.raw", "cmi.score.raw", "cmi.score.scaled"]: return {"value": self.lesson_score * 100} elif name in ["cmi.core.student_id", "cmi.learner_id"]: return {"value": self.get_current_user_attr("edx-platform.user_id")} @@ -260,8 +267,11 @@ def scorm_set_value(self, data, suffix=""): success_status = value elif name == "cmi.completion_status": completion_status = value - elif name in ["cmi.core.score.raw", "cmi.score.raw"] and self.has_score: - lesson_score = float(value) / 100.0 + elif ( + name in ["cmi.core.score.raw", "cmi.score.raw", "cmi.score.scaled"] + and self.has_score + ): + lesson_score = float(value) / 100 elif name == "cmi.progress_measure": completion_percent = float(value) diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/static/html/scormxblock.html b/abstract_scorm_xblock/abstract_scorm_xblock/static/html/scormxblock.html index 7ed0048..4049094 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/static/html/scormxblock.html +++ b/abstract_scorm_xblock/abstract_scorm_xblock/static/html/scormxblock.html @@ -3,7 +3,7 @@
{% if scorm_xblock.has_score %}

- ({{ scorm_xblock.lesson_score }}/{{ scorm_xblock.weight }} {% trans "points" %}) + ({{ scorm_xblock.lesson_score_display }}/{{ scorm_xblock.weight }} {% trans "points" %}) {% trans completion_status %}

{% endif %} diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/static/js/src/scormxblock.js b/abstract_scorm_xblock/abstract_scorm_xblock/static/js/src/scormxblock.js index d76b7f5..acea398 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/static/js/src/scormxblock.js +++ b/abstract_scorm_xblock/abstract_scorm_xblock/static/js/src/scormxblock.js @@ -58,12 +58,21 @@ function ScormXBlock(runtime, element, settings) { } var GetValue = function (cmi_element) { - return $.ajax({ + var response = $.ajax({ type: "POST", url: runtime.handlerUrl(element, "scorm_get_value"), data: JSON.stringify({ name: cmi_element }), async: false, - }).responseText; + }); + + response = JSON.parse(response.responseText); + if (!response.value && cmi_element in settings.scorm_data) { + if ([undefined, null].includes(settings.scorm_data[cmi_element])) { + return ""; + } + return settings.scorm_data[cmi_element]; + } + return response.value; }; var SetValue = function (cmi_element, value) { @@ -74,11 +83,12 @@ function ScormXBlock(runtime, element, settings) { async: true, success: function (response) { if (typeof response.lesson_score != "undefined") { - $(".lesson_score", element).html(response.lesson_score); + $(".lesson_score", element).html(response.lesson_score.toFixed(2)); } $(".completion_status", element).html(response.completion_status); }, }); + return "true"; }; var GetAPI = function () { From dad2708876032d260f6ce15e68e2aceb79ef19ef Mon Sep 17 00:00:00 2001 From: Illia Shestakov Date: Tue, 21 Feb 2023 16:11:40 +0200 Subject: [PATCH 3/4] _get_completion_status -> get_lesson_status in tests --- .../abstract_scorm_xblock/tests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/tests.py b/abstract_scorm_xblock/abstract_scorm_xblock/tests.py index 3402530..c75bc2a 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/tests.py +++ b/abstract_scorm_xblock/abstract_scorm_xblock/tests.py @@ -104,7 +104,7 @@ def test_studio_submit(self, contentstore): self.assertEqual(response.status_code, 404) @mock.patch( - "abstract_scorm_xblock.scormxblock.AbstractScormXBlock._get_completion_status", + "abstract_scorm_xblock.scormxblock.AbstractScormXBlock.get_lesson_status", return_value="completion_status", ) @mock.patch("abstract_scorm_xblock.scormxblock.AbstractScormXBlock._publish_grade") @@ -113,7 +113,7 @@ def test_studio_submit(self, contentstore): {"name": "cmi.completion_status", "value": "failed"}, {"name": "cmi.success_status", "value": "unknown"}, ) - def test_set_status(self, value, _publish_grade, _get_completion_status): + def test_set_status(self, value, _publish_grade, get_lesson_status): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( @@ -121,7 +121,7 @@ def test_set_status(self, value, _publish_grade, _get_completion_status): ) _publish_grade.assert_called_once_with() - _get_completion_status.assert_called_once_with() + get_lesson_status.assert_called_once_with() if value["name"] == "cmi.success_status": self.assertEqual(xblock._success_status, value["value"]) @@ -138,21 +138,21 @@ def test_set_status(self, value, _publish_grade, _get_completion_status): ) @mock.patch( - "abstract_scorm_xblock.scormxblock.AbstractScormXBlock._get_completion_status", + "abstract_scorm_xblock.scormxblock.AbstractScormXBlock.get_lesson_status", return_value="completion_status", ) @ddt.data( {"name": "cmi.core.score.raw", "value": "20"}, {"name": "cmi.score.raw", "value": "20"}, ) - def test_set_lesson_score(self, value, _get_completion_status): + def test_set_lesson_score(self, value, get_lesson_status): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) - _get_completion_status.assert_called_once_with() + get_lesson_status.assert_called_once_with() self.assertEqual(xblock.lesson_score, 0.2) @@ -166,7 +166,7 @@ def test_set_lesson_score(self, value, _get_completion_status): ) @mock.patch( - "abstract_scorm_xblock.scormxblock.AbstractScormXBlock._get_completion_status", + "abstract_scorm_xblock.scormxblock.AbstractScormXBlock.get_lesson_status", return_value="completion_status", ) @ddt.data( @@ -174,14 +174,14 @@ def test_set_lesson_score(self, value, _get_completion_status): {"name": "cmi.location", "value": 2}, {"name": "cmi.suspend_data", "value": [1, 2]}, ) - def test_set_other_scorm_values(self, value, _get_completion_status): + def test_set_other_scorm_values(self, value, get_lesson_status): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) - _get_completion_status.assert_called_once() + get_lesson_status.assert_called_once() self.assertEqual(xblock._scorm_data[value["name"]], value["value"]) From c14c01703fd521a8ed5688e1690ae156b05f0be4 Mon Sep 17 00:00:00 2001 From: Chiruzzi Marco Date: Tue, 21 Feb 2023 16:06:51 +0100 Subject: [PATCH 4/4] Fix tests and refactor some code --- .github/workflows/python-app.yml | 1 + .../abstract_scorm_xblock/scormxblock.py | 76 +++++++++++++------ .../abstract_scorm_xblock/tests.py | 49 +++++------- derex_project/derex.config.yaml | 2 +- derex_project/settings/base.py | 2 +- 5 files changed, 75 insertions(+), 55 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index aafba3d..420b4e2 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -35,6 +35,7 @@ jobs: derex reset-rabbitmq # We can't use `derex create-bucket` because github CI doesn't allocate a TTY docker run --rm --network derex --entrypoint /bin/sh minio/mc -c 'mc config host add local http://minio:80 minio_derex "ICDTE0ZnlbIR7r6/qE81nkF7Kshc2gXYv6fJR4I/HKPeTbxEeB3nxC85Ne6C844hEaaC2+KHBRIOzGou9leulZ7t" --api s3v4; set -ex; mc mb --ignore-existing local/scorm; mc policy set download local/scorm/profile-images' + derex settings base derex build project - name: Run tests run: | diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py b/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py index 3cf91d1..063068e 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py +++ b/abstract_scorm_xblock/abstract_scorm_xblock/scormxblock.py @@ -109,6 +109,22 @@ class AbstractScormXBlock(XBlock, CompletableXBlockMixin): # save completion_status for SCORM_2004 _lesson_status = String(scope=Scope.user_state, default="not attempted") _success_status = String(scope=Scope.user_state, default="unknown") + + """ + Fields description from + https://scorm.com/scorm-explained/technical-scorm/run-time/run-time-reference/ : + + * cmi.learner_id (long_identifier_type (SPM: 4000), RO) Identifies the learner on behalf of whom the SCO was launched + * cmi.location (characterstring (SPM: 1000), RW) The learner's current location in the SCO + * cmi.suspend_data (characterstring (SPM: 4000), RW) Provides space to store and retrieve data between learner sessions + * cmi.completion_status (“completed”, “incomplete”, “not attempted”, “unknown”, RW) Indicates whether the learner has completed the SCO + * cmi.completion_threshold (real(10,7) range (0..1), RO) Used to determine whether the SCO should be considered complete + * cmi.success_status (“passed”, “failed”, “unknown”, RW) Indicates whether the learner has mastered the SCO + * cmi.score.scaled (real (10,7) range (-1..1), RW) Number that reflects the performance of the learner + * cmi.score.raw (real (10,7), RW) Number that reflects the performance of the learner relative to the range bounded by the values of min and max + * cmi.score.min (real (10,7), RW) Minimum value in the range for the raw score + * cmi.score.max (real (10,7), RW) Maximum value in the range for the raw score + """ _scorm_data = Dict(scope=Scope.user_state, default={}) @property @@ -245,11 +261,37 @@ def scorm_get_value(self, data, suffix=""): else: return {"value": self._scorm_data.get(name, "")} + def get_payload( + self, + lesson_score, + lesson_status, + success_status, + completion_status, + ): + payload = {"result": "success"} + if lesson_score: + self.lesson_score = lesson_score + if success_status in ["failed", "unknown"]: + lesson_score = 0 + else: + lesson_score = lesson_score * self.weight + payload.update({"lesson_score": lesson_score}) + + if lesson_status: + self._lesson_status = lesson_status + payload.update({"completion_status": lesson_status}) + + if completion_status: + self.completion_status = completion_status + payload.update({"completion_status": completion_status}) + + return payload + @XBlock.json_handler def scorm_set_value(self, data, suffix=""): - payload = {"result": "success"} name = data.get("name") value = data.get("value") + lesson_score = None lesson_status = None success_status = None @@ -257,6 +299,7 @@ def scorm_set_value(self, data, suffix=""): completion_percent = None self._scorm_data[name] = value + if name in ["cmi.core.lesson_status", "cmi.completion_status"]: lesson_status = value if lesson_status in ["passed", "failed"]: @@ -275,30 +318,21 @@ def scorm_set_value(self, data, suffix=""): elif name == "cmi.progress_measure": completion_percent = float(value) - payload = {"result": "success"} - if lesson_score is not None: - self.lesson_score = lesson_score - if success_status in ["failed", "unknown"]: - lesson_score = 0 - else: - lesson_score = lesson_score * self.weight - payload.update({"lesson_score": lesson_score}) - if lesson_status: - self._lesson_status = lesson_status - payload.update({"completion_status": lesson_status}) - if completion_percent is not None: - self.emit_completion(completion_percent) - if completion_status: - self.completion_status = completion_status - payload.update({"completion_status": completion_status}) - if success_status: - self._success_status = success_status if completion_status == "completed": self.emit_completion(1) + if completion_percent: + self.emit_completion(completion_percent) + if success_status or completion_status == "completed": if self.has_score: self._publish_grade() - return payload + + return self.get_payload( + lesson_score, + lesson_status, + success_status, + completion_status, + ) def set_score(self, score): """ @@ -329,8 +363,6 @@ def get_lesson_status(self): and self._success_status != "unknown" ): lesson_status = self._success_status - if not lesson_status: - lesson_status = "unknown" return lesson_status def _read_scorm_manifest(self, scorm_path): diff --git a/abstract_scorm_xblock/abstract_scorm_xblock/tests.py b/abstract_scorm_xblock/abstract_scorm_xblock/tests.py index c75bc2a..287329f 100644 --- a/abstract_scorm_xblock/abstract_scorm_xblock/tests.py +++ b/abstract_scorm_xblock/abstract_scorm_xblock/tests.py @@ -103,17 +103,13 @@ def test_studio_submit(self, contentstore): response = xblock.studio_submit(mock.Mock(method="POST", params=fields)) self.assertEqual(response.status_code, 404) - @mock.patch( - "abstract_scorm_xblock.scormxblock.AbstractScormXBlock.get_lesson_status", - return_value="completion_status", - ) @mock.patch("abstract_scorm_xblock.scormxblock.AbstractScormXBlock._publish_grade") @ddt.data( {"name": "cmi.core.lesson_status", "value": "completed"}, {"name": "cmi.completion_status", "value": "failed"}, {"name": "cmi.success_status", "value": "unknown"}, ) - def test_set_status(self, value, _publish_grade, get_lesson_status): + def test_set_status(self, value, _publish_grade): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( @@ -121,73 +117,64 @@ def test_set_status(self, value, _publish_grade, get_lesson_status): ) _publish_grade.assert_called_once_with() - get_lesson_status.assert_called_once_with() if value["name"] == "cmi.success_status": self.assertEqual(xblock._success_status, value["value"]) + self.assertEqual( + response.json, + { + "result": "success", + }, + ) else: self.assertEqual(xblock._lesson_status, value["value"]) - self.assertEqual( - response.json, - { - "completion_status": "completion_status", - "lesson_score": 0, - "result": "success", - }, - ) + self.assertEqual( + response.json, + { + "completion_status": value["value"], + "result": "success", + }, + ) - @mock.patch( - "abstract_scorm_xblock.scormxblock.AbstractScormXBlock.get_lesson_status", - return_value="completion_status", - ) @ddt.data( {"name": "cmi.core.score.raw", "value": "20"}, {"name": "cmi.score.raw", "value": "20"}, ) - def test_set_lesson_score(self, value, get_lesson_status): + def test_set_lesson_score(self, value): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) - get_lesson_status.assert_called_once_with() - self.assertEqual(xblock.lesson_score, 0.2) self.assertEqual( response.json, { - "completion_status": "completion_status", - "lesson_score": 0.2, + "lesson_score": float(value["value"]) / 100, "result": "success", }, ) - @mock.patch( - "abstract_scorm_xblock.scormxblock.AbstractScormXBlock.get_lesson_status", - return_value="completion_status", - ) @ddt.data( {"name": "cmi.core.lesson_location", "value": 1}, {"name": "cmi.location", "value": 2}, {"name": "cmi.suspend_data", "value": [1, 2]}, ) - def test_set_other_scorm_values(self, value, get_lesson_status): + def test_set_other_scorm_values(self, value): xblock = self.make_one(has_score=True) response = xblock.scorm_set_value( mock.Mock(method="POST", body=json.dumps(value).encode("utf-8")) ) - get_lesson_status.assert_called_once() - self.assertEqual(xblock._scorm_data[value["name"]], value["value"]) self.assertEqual( response.json, - {"completion_status": "completion_status", "result": "success"}, + {"result": "success"}, ) @ddt.data( diff --git a/derex_project/derex.config.yaml b/derex_project/derex.config.yaml index 8899d15..a3af7ea 100644 --- a/derex_project/derex.config.yaml +++ b/derex_project/derex.config.yaml @@ -1,2 +1,2 @@ project_name: scorm -openedx_version: juniper +openedx_version: koa diff --git a/derex_project/settings/base.py b/derex_project/settings/base.py index e087580..6f9a8e4 100644 --- a/derex_project/settings/base.py +++ b/derex_project/settings/base.py @@ -1,3 +1,3 @@ -from .derex import * # noqa +from derex_django.settings.default import * # noqa X_FRAME_OPTIONS = "SAMEORIGIN"