From 32fa5b2d56833a7ec308f34edb80654677c2d53d Mon Sep 17 00:00:00 2001 From: Zia Fazal Date: Thu, 15 Feb 2024 16:25:42 +0500 Subject: [PATCH 1/4] fix: modifications to remove wanings updated enrollment service fine tuning code and readme Improved enrollment service API Stopping inline iframe usage for now --- README.rst | 215 ++---------------- openedx_cmi5_xblock/openedx_cmi5_xblock.py | 48 ++-- .../static/css/openedx_cmi5_xblock.css | 16 ++ .../static/html/openedx_cmi5_xblock.html | 21 +- openedx_cmi5_xblock/static/html/studio.html | 1 - .../static/js/src/openedx_cmi5_xblock.js | 13 +- openedx_cmi5_xblock/utils/utility.py | 5 +- 7 files changed, 83 insertions(+), 236 deletions(-) diff --git a/README.rst b/README.rst index a476763..8c98828 100644 --- a/README.rst +++ b/README.rst @@ -1,221 +1,36 @@ Openedx CMI5 XBlock ############################# -Xblock to play CMI5 content inside Open edX +Xblock to integrate CMI5 content in Open edX. It provides following features +* Ability to upload CMI5 package or cmi5.xml file +* Parses xAPI statements generated by CMI5 content to set grade and track progress of learner +* Ability to push xAPI statements to a configured LRS -Testing with Docker +Setup ******************** -This XBlock comes with a Docker test environment ready to build, based on the xblock-sdk workbench. To build and run it:: +Install CMI5 XBlock - $ make dev.run + $ pip install openedx-cmi5-xblock -The XBlock SDK Workbench, including this XBlock, will be available on the list of XBlocks at http://localhost:8000 +Enable XBlock in Studio -Installation for tutor -********************** + `Settings -> Advanced Settings -> Advanced Module List` add `openedx_cmi5_xblock` -This method works with `tutor `__. +Use it in any of the units by adding `CMI5 Module` from Advanced blocks list -First, go to your requirements directory:: +Advanced Configuration For sending data to LRS +********************************************** - cd $(tutor config printroot)/env/build/openedx/requirements/ +CMI5 Xblock can be configured to push xAPI statements to a Third-party Learning Record Store. To configure that, use these settings +.. code-block:: python -add the `openedx-cmi5-xblock` repo to the `private.txt`:: - - echo "git+https://github.com/edly-io/openedx-cmi5-xblock.git" >> private.txt - - -and build a new image:: - - tutor images build openedx - - -In your studio, in your desired course, go to Advanced Settings and add `"openedx_cmi5_xblock"` in the Advanced Module List. - -If you want to test the cmi5 content, then `here `__ you can get demo files to test: - -Development -*********** - -There's no need to build a new image, if you just want to play with the xblock. - -First, clone the repo in the requirements directory:: - - cd $(tutor config printroot)/env/build/openedx/requirements/ - git clone git@github.com:edly-io/openedx-cmi5-xblock.git - - -exec to the lms container and install the XBlock:: - - tutor dev exec -it lms bash - cd ../requirements - pip install -e openedx-cmi5-xblock - -If you struggle with lms not displaying your cmi5 content in IFrame, then ``X_FRAME_OPTIONS = "SAMEORIGIN"`` is the settings you need to add a patch for to give access to your lms domain. - -Note: This is not the best practice to develop an XBlock, but it works if you don't want to build dev image. - - -Advanced Configuration For External LRS -*************************************** - -The cmi5 Xblock may be configured to enable Third-party Learning Record Store to keep record of xapi statements outside of LMS. To configure that, add the following to Tutor by creating a `plugin `__:: - - hooks.Filters.ENV_PATCHES.add_item( - ( - "openedx-common-settings", - """ XBLOCK_SETTINGS["CMI5XBlock"] = { "LRS_AUTH_KEY": "", "LRS_AUTH_SECRET": "", "LRS_ENDPOINT": "/lrs//statements/" # ... other settings - }""" - ) - ) - -Note: This method is for enabling External LRS for CMI5-Xblock in Tutor. - - -Translating -************* - -Internationalization (i18n) is when a program is made aware of multiple languages. -Localization (l10n) is adapting a program to local language and cultural habits. - -Use the locale directory to provide internationalized strings for your XBlock project. -For more information on how to enable translations, visit the -`Open edX XBlock tutorial on Internationalization `_. - -This cookiecutter template uses `django-statici18n `_ -to provide translations to static javascript using ``gettext``. - -The included Makefile contains targets for extracting, compiling and validating translatable strings. -The general steps to provide multilingual messages for a Python program (or an XBlock) are: - -1. Mark translatable strings. -2. Run i18n tools to create raw message catalogs. -3. Create language specific translations for each message in the catalogs. -4. Use ``gettext`` to translate strings. - -1. Mark translatable strings -============================= - -Mark translatable strings in python:: - - - from django.utils.translation import ugettext as _ - - # Translators: This comment will appear in the `.po` file. - message = _("This will be marked.") - -See `edx-developer-guide `__ -for more information. - -You can also use ``gettext`` to mark strings in javascript:: - - - // Translators: This comment will appear in the `.po` file. - var message = gettext("Custom message."); - -See `edx-developer-guide `__ -for more information. - -2. Run i18n tools to create Raw message catalogs -================================================= - -This cookiecutter template offers multiple make targets which are shortcuts to -use `edx-i18n-tools `_. - -After marking strings as translatable we have to create the raw message catalogs. -These catalogs are created in ``.po`` files. For more information see -`GNU PO file documentation `_. -These catalogs can be created by running:: - - - $ make extract_translations - -The previous command will create the necessary ``.po`` files under -``openedx-cmi5-xblock/openedx_cmi5_xblock/locale/en/LC_MESSAGES/text.po``. -The ``text.po`` file is created from the ``django-partial.po`` file created by -``django-admin makemessages`` (`makemessages documentation `_), -this is why you will not see a ``django-partial.po`` file. - -3. Create language specific translations -============================================== - -3.1 Add translated strings ---------------------------- - -After creating the raw message catalogs, all translations should be filled out by the translator. -One or more translators must edit the entries created in the message catalog, i.e. the ``.po`` file(s). -The format of each entry is as follows:: - - # translator-comments - A. extracted-comments - #: referenceā€¦ - #, flagā€¦ - #| msgid previous-untranslated-string - msgid 'untranslated message' - msgstr 'mensaje traducido (translated message)' - -For more information see -`GNU PO file documentation `_. - -To use translations from transifex use the follow Make target to pull translations:: - - $ make pull_translations - -See `config instructions `_ for information on how to set up your -transifex credentials. - -See `transifex documentation `_ for more details about integrating -django with transiflex. - -3.2 Compile translations -------------------------- - -Once translations are in place, use the following Make target to compile the translation catalogs ``.po`` into -``.mo`` message files:: - - $ make compile_translations - -The previous command will compile ``.po`` files using -``django-admin compilemessages`` (`compilemessages documentation `_). -After compiling the ``.po`` file(s), ``django-statici18n`` is used to create language specific catalogs. See -``django-statici18n`` `documentation `_ for more information. - -To upload translations to transiflex use the follow Make target:: - - $ make push_translations - -See `config instructions `_ for information on how to set up your -transifex credentials. - -See `transifex documentation `_ for more details about integrating -django with transiflex. - - **Note:** The ``dev.run`` make target will automatically compile any translations. - - **Note:** To check if the source translation files (``.po``) are up-to-date run:: - - $ make detect_changed_source_translations - -4. Use ``gettext`` to translate strings -======================================== - -Django will automatically use ``gettext`` and the compiled translations to translate strings. - -Troubleshooting -**************** - -If there are any errors compiling ``.po`` files run the following command to validate your ``.po`` files:: - - $ make validate + } -See `django's i18n troubleshooting documentation -`_ -for more information. diff --git a/openedx_cmi5_xblock/openedx_cmi5_xblock.py b/openedx_cmi5_xblock/openedx_cmi5_xblock.py index 6587369..6802f29 100644 --- a/openedx_cmi5_xblock/openedx_cmi5_xblock.py +++ b/openedx_cmi5_xblock/openedx_cmi5_xblock.py @@ -23,7 +23,7 @@ from xblock.completable import CompletableXBlockMixin from xblock.core import XBlock from xblock.fields import Boolean, DateTime, Dict, Float, Integer, List, Scope, String -from xblock.fragment import Fragment +from web_fragments.fragment import Fragment from openedx_cmi5_xblock.utils.utility import ( get_request_body, @@ -129,6 +129,12 @@ class CMI5XBlock(XBlock, CompletableXBlockMixin): scope=Scope.content, ) + au_has_any_window = Boolean( + display_name=_('Has one of the AUs launch method is AnyWindow'), + default=False, + scope=Scope.settings + ) + has_author_view = True def author_view(self, context=None): @@ -152,8 +158,7 @@ def studio_view(self, context=None): 'field_width': self.fields['width'], 'field_height': self.fields['height'], 'cmi5_xblock': self - } - + } studio_context.update(context or {}) template = render_template('static/html/studio.html', studio_context) frag = Fragment(template) @@ -193,16 +198,8 @@ def lrs_endpoint(self, request, _suffix): """ credentials = self.get_credentials() - if request.params.get('statementId') and request.method == 'PUT' and credentials["EXTERNAL_LRS_URL"]: + if request.params.get('statementId') and request.method == 'PUT': statement_data = get_request_body(request) - # send statements to external lrs. - send_xapi_to_external_lrs( - statement_data, - credentials["EXTERNAL_LRS_URL"], - credentials["LRS_AUTH_KEY"], - credentials["LRS_AUTH_SECRET"] - ) - lesson_status = statement_data.get('verb').get('display').get('en') object_categories = statement_data.get('context', {}).get('contextActivities', {}).get('category') @@ -216,6 +213,15 @@ def lrs_endpoint(self, request, _suffix): self.lesson_status = lesson_status self.emit_completion(1.0) + # send statements to external lrs. + if credentials["EXTERNAL_LRS_URL"]: + send_xapi_to_external_lrs( + statement_data, + credentials["EXTERNAL_LRS_URL"], + credentials["LRS_AUTH_KEY"], + credentials["LRS_AUTH_SECRET"] + ) + return Response(status=204) elif request.params.get('stateId'): @@ -355,11 +361,12 @@ def get_grade(self): def get_erollment_id(self): """Retrieves the enrollment ID of the current user for the XBlock's course.""" - user_id = self.get_current_user_attr('edx-platform.user_id') - course_id = self.runtime.course_id + django_user = self.runtime.service(self, 'user').get_current_django_user() + course_id = self.scope_ids.usage_id.context_key + try: - enrollment = self.runtime.service(self, 'enrollments').get_active_enrollment_of_user_by_course( - user_id, course_id + enrollment = self.runtime.service(self, 'enrollments').get_active_enrollments_by_course_and_user( + course_id, django_user ) return enrollment.id except Exception as err: @@ -522,17 +529,24 @@ def update_package_fields(self): if au_elements: self.index_page_path = au_elements[0].find('./{prefix}url'.format(prefix=prefix)).text au_data_list = [] + launch_methods = [] for au in au_elements: au_url = au.find('./{prefix}url'.format(prefix=prefix)).text + au_title = au.find('./{prefix}title/{prefix}langstring'.format(prefix=prefix)).text launch_method = au.get('launchMethod', 'AnyWindow') + launch_methods.append(launch_method) au_data = { - 'url': au_url, + 'url': au_url.strip(), + 'title': au_title, 'launch_method': launch_method } au_data_list.append(au_data) self.au_urls = au_data_list + # TODO: Uncomment the code below when we have figured out a workaround of limitations imposed by + # browsers while loading iframe from different domain than host + # self.au_has_any_window = True if 'AnyWindow' in launch_methods else False else: self.index_page_path = self.find_relative_file_path('index.html') diff --git a/openedx_cmi5_xblock/static/css/openedx_cmi5_xblock.css b/openedx_cmi5_xblock/static/css/openedx_cmi5_xblock.css index d801160..693f9a8 100644 --- a/openedx_cmi5_xblock/static/css/openedx_cmi5_xblock.css +++ b/openedx_cmi5_xblock/static/css/openedx_cmi5_xblock.css @@ -7,3 +7,19 @@ .openedx_cmi5_xblock_block p { cursor: pointer; } + +.cmi5-column { + float: left; +} + +.cmi5-nav { + width: 25%; +} + +.cmi5-content { + width: 75%; +} + +.cmi5-content iframe { + border: 0; +} \ No newline at end of file diff --git a/openedx_cmi5_xblock/static/html/openedx_cmi5_xblock.html b/openedx_cmi5_xblock/static/html/openedx_cmi5_xblock.html index 4dc0e88..543f83b 100644 --- a/openedx_cmi5_xblock/static/html/openedx_cmi5_xblock.html +++ b/openedx_cmi5_xblock/static/html/openedx_cmi5_xblock.html @@ -1,24 +1,22 @@ {% load i18n %}
-

{{title}}

-
- + {% if cmi5_xblock.has_score %} +

({{ cmi5_xblock.lesson_score}}/{{ cmi5_xblock.weight | floatformat:1 }} {% trans "points" %})

+ {% endif %} {% if index_page_url %} -
{% trans "Available Assignables:" %}
+ +
    {% for au_url in au_urls %}
  1. - - {% trans "AU No." %} {{ forloop.counter }} - - ({%trans "Launch: "%} {{au_url.launch_method}}) + {{au_url.title}}
  2. - {% endfor %}
- -
+
+ {% if cmi5_xblock.au_has_any_window %} +
+ {% endif %} {% elif message %}

{{ message }}

{% endif %} diff --git a/openedx_cmi5_xblock/static/html/studio.html b/openedx_cmi5_xblock/static/html/studio.html index 90ac2da..e3f0471 100644 --- a/openedx_cmi5_xblock/static/html/studio.html +++ b/openedx_cmi5_xblock/static/html/studio.html @@ -107,4 +107,3 @@
-v diff --git a/openedx_cmi5_xblock/static/js/src/openedx_cmi5_xblock.js b/openedx_cmi5_xblock/static/js/src/openedx_cmi5_xblock.js index 22c3c66..92275bb 100644 --- a/openedx_cmi5_xblock/static/js/src/openedx_cmi5_xblock.js +++ b/openedx_cmi5_xblock/static/js/src/openedx_cmi5_xblock.js @@ -13,15 +13,18 @@ function CMI5XBlock(runtime, element) { $('ol a').click(function(event) { event.preventDefault(); var href = $(this).attr('href'); - var liText = $(this).closest('li').text().trim(); - updateIframeSrc(href, liText); + var launchMethod = $(this).data("launch-method"); + updateIframeSrc(href, launchMethod); }); - function updateIframeSrc(href, liText) { - if (liText.includes('AnyWindow')) { + function updateIframeSrc(href, launchMethod) { + // TODO: Uncomment the code below when we have figured out a workaround of limitations imposed by + // browsers while loading iframe from different domain than host + launchMethod = 'OwnWindow'; + if (launchMethod === 'AnyWindow') { $('.cmi5-embedded').attr('src', href); - } else if (liText.includes('OwnWindow')) { + } else { window.open(href, '_blank'); } } diff --git a/openedx_cmi5_xblock/utils/utility.py b/openedx_cmi5_xblock/utils/utility.py index f0fdf2f..729f127 100644 --- a/openedx_cmi5_xblock/utils/utility.py +++ b/openedx_cmi5_xblock/utils/utility.py @@ -5,6 +5,7 @@ import requests from django.core.validators import URLValidator +from django.core.exceptions import ValidationError from requests.auth import HTTPBasicAuth from webob import Response @@ -21,8 +22,8 @@ def is_url(path): try: validator = URLValidator() validator(path) - except Exception as err: - logger.error("Invalid URL (%s): %s", path, err) + except ValidationError as err: + logger.debug("Invalid URL (%s): %s", path, err) return False return True From 2facbff2859c91154e1ff3aea488c2fe9faf0586 Mon Sep 17 00:00:00 2001 From: Zia Fazal Date: Thu, 15 Feb 2024 16:46:05 +0500 Subject: [PATCH 2/4] fix: fix quality violation --- openedx_cmi5_xblock/openedx_cmi5_xblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_cmi5_xblock/openedx_cmi5_xblock.py b/openedx_cmi5_xblock/openedx_cmi5_xblock.py index 6802f29..409f5fd 100644 --- a/openedx_cmi5_xblock/openedx_cmi5_xblock.py +++ b/openedx_cmi5_xblock/openedx_cmi5_xblock.py @@ -158,7 +158,7 @@ def studio_view(self, context=None): 'field_width': self.fields['width'], 'field_height': self.fields['height'], 'cmi5_xblock': self - } + } studio_context.update(context or {}) template = render_template('static/html/studio.html', studio_context) frag = Fragment(template) From 33c403ec343bdeea7268ce563e742d0eb3dcf922 Mon Sep 17 00:00:00 2001 From: Zia Fazal Date: Thu, 15 Feb 2024 16:51:22 +0500 Subject: [PATCH 3/4] fix: fix quality violation --- openedx_cmi5_xblock/utils/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_cmi5_xblock/utils/utility.py b/openedx_cmi5_xblock/utils/utility.py index 729f127..003a20d 100644 --- a/openedx_cmi5_xblock/utils/utility.py +++ b/openedx_cmi5_xblock/utils/utility.py @@ -4,8 +4,8 @@ import logging import requests -from django.core.validators import URLValidator from django.core.exceptions import ValidationError +from django.core.validators import URLValidator from requests.auth import HTTPBasicAuth from webob import Response From d1a63ba713a4dae047600c3aa66ac39177dd5e44 Mon Sep 17 00:00:00 2001 From: Zia Fazal Date: Thu, 15 Feb 2024 16:56:17 +0500 Subject: [PATCH 4/4] fix: fix quality violation --- openedx_cmi5_xblock/openedx_cmi5_xblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_cmi5_xblock/openedx_cmi5_xblock.py b/openedx_cmi5_xblock/openedx_cmi5_xblock.py index 409f5fd..981fbc0 100644 --- a/openedx_cmi5_xblock/openedx_cmi5_xblock.py +++ b/openedx_cmi5_xblock/openedx_cmi5_xblock.py @@ -19,11 +19,11 @@ from django.utils import timezone from django.utils.module_loading import import_string from six import string_types +from web_fragments.fragment import Fragment from webob import Response from xblock.completable import CompletableXBlockMixin from xblock.core import XBlock from xblock.fields import Boolean, DateTime, Dict, Float, Integer, List, Scope, String -from web_fragments.fragment import Fragment from openedx_cmi5_xblock.utils.utility import ( get_request_body,