From a24bfb38ca799c49de44e18d48caaf780ceebf84 Mon Sep 17 00:00:00 2001 From: oksana-slu Date: Fri, 7 Aug 2020 16:47:43 +0300 Subject: [PATCH 01/13] Change the scorm save to default_storage --- scormxblock/scormxblock.py | 113 ++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 58 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index a284f325..79f3ebf4 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -4,32 +4,27 @@ import os import logging import pkg_resources -import shutil import xml.etree.ElementTree as ET from functools import partial -from django.conf import settings + +import zipfile from django.core.files import File from django.core.files.storage import default_storage from django.template import Context, Template from django.utils import timezone from webob import Response -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from xblock.core import XBlock from xblock.fields import Scope, String, Float, Boolean, Dict, DateTime, Integer from xblockutils.resources import ResourceLoader from web_fragments.fragment import Fragment - # Make '_' a no-op so we can scrape strings _ = lambda text: text loader = ResourceLoader(__name__) log = logging.getLogger(__name__) -SCORM_ROOT = os.path.join(settings.MEDIA_ROOT, 'scorm') -SCORM_URL = os.path.join(settings.MEDIA_URL, 'scorm') - @XBlock.needs('i18n') class ScormXBlock(XBlock): @@ -98,8 +93,6 @@ class ScormXBlock(XBlock): scope=Scope.settings ) - has_author_view = True - def resource_string(self, path): """Handy helper for getting resources from our kit.""" data = pkg_resources.resource_string(__name__, path) @@ -134,15 +127,6 @@ def studio_view(self, context=None): frag.initialize_js('ScormStudioXBlock') return frag - def author_view(self, context=None): - html = loader.render_django_template( - "static/html/author_view.html", - context=context, - i18n_service=self.runtime.service(self, 'i18n') - ) - frag = Fragment(html) - return frag - @XBlock.handler def studio_submit(self, request, suffix=''): self.display_name = request.params['display_name'] @@ -154,42 +138,31 @@ def studio_submit(self, request, suffix=''): if hasattr(request.params['file'], 'file'): scorm_file = request.params['file'].file + if default_storage.exists(self.folder_base_path): + log.info( + 'Removing previously uploaded "%s"', self.folder_base_path + ) + self.recursive_delete(self.folder_base_path) + # First, save scorm file in the storage for mobile clients self.scorm_file_meta['sha1'] = self.get_sha1(scorm_file) self.scorm_file_meta['name'] = scorm_file.name self.scorm_file_meta['path'] = path = self._file_storage_path() self.scorm_file_meta['last_updated'] = timezone.now().strftime(DateTime.DATETIME_FORMAT) - if default_storage.exists(path): - log.info('Removing previously uploaded "{}"'.format(path)) - default_storage.delete(path) - default_storage.save(path, File(scorm_file)) self.scorm_file_meta['size'] = default_storage.size(path) log.info('"{}" file stored at "{}"'.format(scorm_file, path)) - # Check whether SCORM_ROOT exists - if not os.path.exists(SCORM_ROOT): - os.mkdir(SCORM_ROOT) - - # Now unpack it into SCORM_ROOT to serve to students later - path_to_file = os.path.join(SCORM_ROOT, self.location.block_id) - - if os.path.exists(path_to_file): - shutil.rmtree(path_to_file) - - if hasattr(scorm_file, 'temporary_file_path'): - os.system('unzip {} -d {}'.format(scorm_file.temporary_file_path(), path_to_file)) - else: - temporary_path = os.path.join(SCORM_ROOT, scorm_file.name) - temporary_zip = open(temporary_path, 'wb') - scorm_file.open() - temporary_zip.write(scorm_file.read()) - temporary_zip.close() - os.system('unzip {} -d {}'.format(temporary_path, path_to_file)) - os.remove(temporary_path) + # Then, extract zip file + with zipfile.ZipFile(scorm_file, "r") as scorm_zipfile: + for zipinfo in scorm_zipfile.infolist(): + default_storage.save( + os.path.join(self.folder_path, zipinfo.filename), + scorm_zipfile.open(zipinfo.filename), + ) - self.set_fields_xblock(path_to_file) + self.set_fields_xblock() return Response(json.dumps({'result': 'success'}), content_type='application/json', charset="utf8") @@ -271,12 +244,7 @@ def get_context_studio(self): def get_context_student(self): scorm_file_path = '' if self.scorm_file: - scheme = 'https' if settings.HTTPS == 'on' else 'http' - scorm_file_path = '{}://{}{}'.format( - scheme, - configuration_helpers.get_value('site_domain', settings.ENV_TOKENS.get('LMS_BASE')), - self.scorm_file - ) + scorm_file_path = default_storage.url(self.scorm_file) return { 'scorm_file_path': scorm_file_path, @@ -289,15 +257,19 @@ def render_template(self, template_path, context): template = Template(template_str) return template.render(Context(context)) - def set_fields_xblock(self, path_to_file): + def set_fields_xblock(self): self.path_index_page = 'index.html' + + imsmanifest_path = os.path.join(self.folder_path, "imsmanifest.xml") try: - tree = ET.parse('{}/imsmanifest.xml'.format(path_to_file)) + imsmanifest_file = default_storage.open(imsmanifest_path) except IOError: pass else: + tree = ET.parse(imsmanifest_file) + imsmanifest_file.seek(0) namespace = '' - for node in [node for _, node in ET.iterparse('{}/imsmanifest.xml'.format(path_to_file), events=['start-ns'])]: + for node in [node for _, node in ET.iterparse(imsmanifest_file, events=['start-ns'])]: if node[0] == '': namespace = node[1] break @@ -317,7 +289,7 @@ def set_fields_xblock(self, path_to_file): else: self.version_scorm = 'SCORM_12' - self.scorm_file = os.path.join(SCORM_URL, '{}/{}'.format(self.location.block_id, self.path_index_page)) + self.scorm_file = os.path.join(self.folder_path, self.path_index_page) def get_completion_status(self): completion_status = self.lesson_status @@ -330,15 +302,28 @@ def _file_storage_path(self): Get file path of storage. """ path = ( - '{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}' - '/{sha1}{ext}'.format( - loc=self.location, - sha1=self.scorm_file_meta['sha1'], + '{folder_path}{ext}'.format( + folder_path=self.folder_path, ext=os.path.splitext(self.scorm_file_meta['name'])[1] ) ) return path + @property + def folder_base_path(self): + """ + Path to the folder where packages will be extracted. + """ + return os.path.join(self.location.block_type, self.location.course, self.location.block_id) + + @property + def folder_path(self): + """ + This path needs to depend on the content of the scorm package. Otherwise, + served media files might become stale when the package is update. + """ + return os.path.join(self.folder_base_path, self.scorm_file_meta["sha1"]) + def get_sha1(self, file_descriptor): """ Get file hex digest (fingerprint). @@ -368,6 +353,18 @@ def student_view_data(self): } return {} + def recursive_delete(self, root): + """ + Recursively delete the contents of a directory in the Django default storage. + Unfortunately, this will not delete empty folders, as the default FileSystemStorage + implementation does not allow it. + """ + directories, files = default_storage.listdir(root) + for directory in directories: + self.recursive_delete(os.path.join(root, directory)) + for f in files: + default_storage.delete(os.path.join(root, f)) + @staticmethod def workbench_scenarios(): """A canned scenario for display in the workbench.""" @@ -377,4 +374,4 @@ def workbench_scenarios(): """), - ] \ No newline at end of file + ] From 0335105c112e35591291b7afd53eb5f30e040569 Mon Sep 17 00:00:00 2001 From: oksana-slu Date: Tue, 18 Aug 2020 16:46:50 +0300 Subject: [PATCH 02/13] Fix IOError("%s exists and is not a directory." % directory) --- scormxblock/scormxblock.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index 79f3ebf4..20697a5f 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -157,10 +157,11 @@ def studio_submit(self, request, suffix=''): # Then, extract zip file with zipfile.ZipFile(scorm_file, "r") as scorm_zipfile: for zipinfo in scorm_zipfile.infolist(): - default_storage.save( - os.path.join(self.folder_path, zipinfo.filename), - scorm_zipfile.open(zipinfo.filename), - ) + if not zipinfo.filename.endswith("/"): + default_storage.save( + os.path.join(self.folder_path, zipinfo.filename), + scorm_zipfile.open(zipinfo.filename), + ) self.set_fields_xblock() From 49272145b6c8113d8d1b8a9bc0bc26338dd7ab8e Mon Sep 17 00:00:00 2001 From: oksana-slu Date: Mon, 31 Aug 2020 14:18:20 +0300 Subject: [PATCH 03/13] Fix save scorm on s3 --- scormxblock/scormxblock.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index 20697a5f..dd6cbb97 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -1,3 +1,4 @@ +import cStringIO import json import hashlib import re @@ -158,10 +159,13 @@ def studio_submit(self, request, suffix=''): with zipfile.ZipFile(scorm_file, "r") as scorm_zipfile: for zipinfo in scorm_zipfile.infolist(): if not zipinfo.filename.endswith("/"): + zip_file = cStringIO.StringIO() + zip_file.write(scorm_zipfile.open(zipinfo.filename).read()) default_storage.save( os.path.join(self.folder_path, zipinfo.filename), - scorm_zipfile.open(zipinfo.filename), + zip_file, ) + zip_file.close() self.set_fields_xblock() From 3ecc86f6363a9dcbfe9adfc353b05f5ff57d070a Mon Sep 17 00:00:00 2001 From: Maxim Starodubcev Date: Wed, 2 Sep 2020 16:47:16 +0300 Subject: [PATCH 04/13] Add s3 file handler --- scormxblock/scormxblock.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index dd6cbb97..7cc108ed 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -6,6 +6,7 @@ import logging import pkg_resources import xml.etree.ElementTree as ET +import mimetypes from functools import partial @@ -15,6 +16,7 @@ from django.template import Context, Template from django.utils import timezone from webob import Response +from storages.backends.s3boto import S3BotoStorage from xblock.core import XBlock from xblock.fields import Scope, String, Float, Boolean, Dict, DateTime, Integer @@ -27,6 +29,24 @@ log = logging.getLogger(__name__) +class FileIter(object): + def __init__(self, _file, _type='application/octet-stream'): + self._file = _file + self.wrapper = lambda d: d + if _type.startswith('text'): + self.wrapper = lambda d: unicode(d, 'utf-8', 'replace') + + def __iter__(self): + try: + while True: + data = self._file.read(65536) + if not data: + return + yield self.wrapper(data) + finally: + self._file.close() + + @XBlock.needs('i18n') class ScormXBlock(XBlock): @@ -249,7 +269,10 @@ def get_context_studio(self): def get_context_student(self): scorm_file_path = '' if self.scorm_file: - scorm_file_path = default_storage.url(self.scorm_file) + if isinstance(default_storage, S3BotoStorage): + scorm_file_path = self.runtime.handler_url(self, 's3_file', self.scorm_file) + else: + scorm_file_path = default_storage.url(self.scorm_file) return { 'scorm_file_path': scorm_file_path, @@ -257,6 +280,15 @@ def get_context_student(self): 'scorm_xblock': self } + @XBlock.handler + def s3_file(self, request, suffix=''): + filename = suffix.split('?')[0] + _type, encoding = mimetypes.guess_type(filename) + _type = _type or 'application/octet-stream' + res = Response(content_type=_type) + res.app_iter = FileIter(default_storage.open(filename, 'rb'), _type) + return res + def render_template(self, template_path, context): template_str = self.resource_string(template_path) template = Template(template_str) From aff203b7c2ad676309dda4265ca29024314278cd Mon Sep 17 00:00:00 2001 From: Zherebkin <51083437+Zherebkin@users.noreply.github.com> Date: Fri, 23 Oct 2020 11:32:43 +0300 Subject: [PATCH 05/13] Tests' fixes (#32) * Tests' fixes * Add extra asserts for zip file saving Co-authored-by: Vladyslav Zherebkin --- scormxblock/tests.py | 61 ++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/scormxblock/tests.py b/scormxblock/tests.py index 200e9ca4..406413b9 100644 --- a/scormxblock/tests.py +++ b/scormxblock/tests.py @@ -13,6 +13,24 @@ @ddt class ScormXBlockTests(unittest.TestCase): + class MockZipf: + def __init__(self): + self.files = [mock.Mock(filename='foo.csv')] + + def __iter__(self): + return iter(self.files) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return True + + def infolist(self): + return self.files + + def open(self, *args, **kwargs): + return self.files[0].filename def make_one(self, **kw): """ @@ -39,7 +57,7 @@ def test_fields_xblock(self): self.assertEqual(block.data_scorm, {}) self.assertEqual(block.lesson_score, 0) self.assertEqual(block.weight, 1) - self.assertEqual(block.has_score, False) + self.assertTrue(block.has_score) self.assertEqual(block.icon_class, 'video') self.assertEqual(block.width, None) self.assertEqual(block.height, 450) @@ -63,23 +81,31 @@ def test_save_settings_scorm(self): self.assertEqual(block.height, 450) @freeze_time("2018-05-01") + @mock.patch('scormxblock.ScormXBlock.recursive_delete') @mock.patch('scormxblock.ScormXBlock.set_fields_xblock') - @mock.patch('scormxblock.scormxblock.shutil') - @mock.patch('scormxblock.scormxblock.SCORM_ROOT') @mock.patch('scormxblock.scormxblock.os') - @mock.patch('scormxblock.scormxblock.zipfile') + @mock.patch('scormxblock.scormxblock.zipfile.ZipFile') @mock.patch('scormxblock.scormxblock.File', return_value='call_file') @mock.patch('scormxblock.scormxblock.default_storage') @mock.patch('scormxblock.ScormXBlock._file_storage_path', return_value='file_storage_path') @mock.patch('scormxblock.ScormXBlock.get_sha1', return_value='sha1') - def test_save_scorm_zipfile(self, get_sha1, file_storage_path, default_storage, mock_file, zipfile, - mock_os, SCORM_ROOT, shutil, set_fields_xblock): + def test_save_scorm_zipfile( + self, + get_sha1, + file_storage_path, + default_storage, + mock_file, + mock_zipfile, + mock_os, + set_fields_xblock, + recursive_delete, + ): block = self.make_one() mock_file_object = mock.Mock() mock_file_object.configure_mock(name='scorm_file_name') default_storage.configure_mock(size=mock.Mock(return_value='1234')) mock_os.configure_mock(path=mock.Mock(join=mock.Mock(return_value='path_join'))) - + mock_zipfile.return_value = ScormXBlockTests.MockZipf() fields = { 'display_name': 'Test Block', 'has_score': 'True', @@ -100,18 +126,13 @@ def test_save_scorm_zipfile(self, get_sha1, file_storage_path, default_storage, get_sha1.assert_called_once_with(mock_file_object) file_storage_path.assert_called_once_with() - default_storage.exists.assert_called_once_with('file_storage_path') - default_storage.delete.assert_called_once_with('file_storage_path') - default_storage.save.assert_called_once_with('file_storage_path', 'call_file') + default_storage.exists.assert_called_once_with('path_join') + recursive_delete.assert_called_once_with('path_join') + default_storage.save.assert_any_call('file_storage_path', 'call_file') mock_file.assert_called_once_with(mock_file_object) - self.assertEqual(block.scorm_file_meta, expected_scorm_file_meta) - - zipfile.ZipFile.assert_called_once_with(mock_file_object, 'r') - mock_os.path.join.assert_called_once_with(SCORM_ROOT, 'block_id') - mock_os.path.exists.assert_called_once_with('path_join') - shutil.rmtree.assert_called_once_with('path_join') - set_fields_xblock.assert_called_once_with('path_join') + default_storage.save.assert_any_call('path_join', 'foo.csv') + set_fields_xblock.assert_called_once_with() def test_build_file_storage_path(self): block = self.make_one( @@ -122,13 +143,14 @@ def test_build_file_storage_path(self): self.assertEqual( file_storage_path, - 'org/course/block_type/block_id/sha1.html' + 'block_type/course/block_id/sha1.html' ) @mock.patch('scormxblock.ScormXBlock._file_storage_path', return_value='file_storage_path') @mock.patch('scormxblock.scormxblock.default_storage') def test_student_view_data(self, default_storage, file_storage_path): block = self.make_one( + scorm_file="url_zip_file", scorm_file_meta={'last_updated': '2018-05-01', 'size': 1234} ) default_storage.configure_mock(url=mock.Mock(return_value='url_zip_file')) @@ -142,7 +164,8 @@ def test_student_view_data(self, default_storage, file_storage_path): { 'last_modified': '2018-05-01', 'scorm_data': 'url_zip_file', - 'size': 1234 + 'size': 1234, + 'index_page': None } ) From c593843b446f6e9ea29eedf3e789aa0925df9d4c Mon Sep 17 00:00:00 2001 From: Maxim Starodubcev Date: Fri, 23 Jul 2021 14:39:00 +0300 Subject: [PATCH 06/13] Fix s3 file uploading for python3 --- scormxblock/scormxblock.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index 7cc108ed..afa81b1d 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -1,4 +1,7 @@ -import cStringIO +try: + from cStringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO import json import hashlib import re @@ -165,21 +168,16 @@ def studio_submit(self, request, suffix=''): ) self.recursive_delete(self.folder_base_path) - # First, save scorm file in the storage for mobile clients self.scorm_file_meta['sha1'] = self.get_sha1(scorm_file) self.scorm_file_meta['name'] = scorm_file.name self.scorm_file_meta['path'] = path = self._file_storage_path() self.scorm_file_meta['last_updated'] = timezone.now().strftime(DateTime.DATETIME_FORMAT) - default_storage.save(path, File(scorm_file)) - self.scorm_file_meta['size'] = default_storage.size(path) - log.info('"{}" file stored at "{}"'.format(scorm_file, path)) - - # Then, extract zip file + # First, extract zip file with zipfile.ZipFile(scorm_file, "r") as scorm_zipfile: for zipinfo in scorm_zipfile.infolist(): if not zipinfo.filename.endswith("/"): - zip_file = cStringIO.StringIO() + zip_file = BytesIO() zip_file.write(scorm_zipfile.open(zipinfo.filename).read()) default_storage.save( os.path.join(self.folder_path, zipinfo.filename), @@ -187,6 +185,13 @@ def studio_submit(self, request, suffix=''): ) zip_file.close() + scorm_file.seek(0) + + # Then, save scorm file in the storage for mobile clients + default_storage.save(path, File(scorm_file)) + self.scorm_file_meta['size'] = default_storage.size(path) + log.info('"{}" file stored at "{}"'.format(scorm_file, path)) + self.set_fields_xblock() return Response(json.dumps({'result': 'success'}), content_type='application/json', charset="utf8") From d1f7fbabc1b7832a4d4d35ac873adb0bf2a1bc80 Mon Sep 17 00:00:00 2001 From: Vladislav Zhuravel Date: Wed, 4 May 2022 19:06:32 +0300 Subject: [PATCH 07/13] feat: [MON-2266] add text.po --- .../translations/uk/LC_MESSAGES/text.po | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 scormxblock/translations/uk/LC_MESSAGES/text.po diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.po b/scormxblock/translations/uk/LC_MESSAGES/text.po new file mode 100644 index 00000000..a3782be4 --- /dev/null +++ b/scormxblock/translations/uk/LC_MESSAGES/text.po @@ -0,0 +1,88 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-04 12:04-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:57 +msgid "Display Name" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:58 +msgid "Display name for this module" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:63 +msgid "Upload scorm file" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:67 +msgid "Path to the index page in scorm file" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:99 +msgid "Scored" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:100 +msgid "" +"Select False if this component will not receive a numerical score from the " +"Scorm" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:109 +msgid "Display Width (px)" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:110 +msgid "Width of iframe, if empty, the default 100" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:114 +msgid "Display Height (px)" +msgstr "" + +#: edx_xblock_scorm/scormxblock/scormxblock.py:115 +msgid "Height of iframe" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/author_view.html:3 +msgid "Look in LMS" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:8 +msgid "points" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:16 +msgid "Full screen" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:19 +msgid "Exit full screen" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/studio.html:18 +msgid "Currently:" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/studio.html:51 +msgid "Save" +msgstr "" + +#: edx_xblock_scorm/scormxblock/static/html/studio.html:54 +msgid "Cancel" +msgstr "" From cac39ecb881ae3068c1a66906161c59f7a9ec62f Mon Sep 17 00:00:00 2001 From: Vladislav Zhuravel Date: Wed, 4 May 2022 19:14:51 +0300 Subject: [PATCH 08/13] feat: [MON-2266] add translates and compile .mo --- .../translations/uk/LC_MESSAGES/text.mo | Bin 0 -> 1479 bytes .../translations/uk/LC_MESSAGES/text.po | 32 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 scormxblock/translations/uk/LC_MESSAGES/text.mo diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.mo b/scormxblock/translations/uk/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..3f0d8453c13b6c7c2dacf39a9e373eff87950ae2 GIT binary patch literal 1479 zcmZvb&u<$=6vqcBKkD+UL8ub+@Kg!Zl5CnH4v{G+#@@76*LGw(MeS{|$Iifd$J!mI z$*pNBL?uK4(Q-fqA#vaYNn3-{G=^J`%u1Z$AK-+<5sB|xr%59^(#&Vy%$xVV?`igr zv3=JB#zD-(nBQR@!kl;n3&uy_6W|pv1AYp=0Dc8N4gLs@fph!8P)I z9H;#NQhkt@*7qE5U~sM{)34FOT2WFu_Vb$e!-fn*z86I@G{JIK6ja;|{AD^VReQnE z(eBc*oxQT(kvqn_s%aMXo`*Q6mX63QsF*ss*$bp_mz1GaF9;}ZL{f%g_JQy1EV@Oi z6>)S=>kmkUVQAJ5VhP_-hKBBRbqL{fK_Pyq4Mnmc)uJSy z!d^#4s^N29grHWWJGdr^BX-1mH_(1FJ?l_QLFlNU9Vru>Ms~Vpc_O{mlr6s(7&>#;h2f}^}tRmh( z-^2T&9l*TrJxVaN>)2njKOyoLi2ZT0l3b$aaJyu~NNew}=nHm=WdV2;j{>s%lmN&kQ&6O>$msP+Ffiv#xwHK#F>Pq?G# o#E?6~xwy0FD>t4COtsYVm1>ES%h=kmn|s^+&_X_g=O(272XECIpa1{> literal 0 HcmV?d00001 diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.po b/scormxblock/translations/uk/LC_MESSAGES/text.po index a3782be4..cf20d563 100644 --- a/scormxblock/translations/uk/LC_MESSAGES/text.po +++ b/scormxblock/translations/uk/LC_MESSAGES/text.po @@ -19,15 +19,15 @@ msgstr "" #: edx_xblock_scorm/scormxblock/scormxblock.py:57 msgid "Display Name" -msgstr "" +msgstr "Назва завдання" #: edx_xblock_scorm/scormxblock/scormxblock.py:58 msgid "Display name for this module" -msgstr "" +msgstr "Відображається на платформі" #: edx_xblock_scorm/scormxblock/scormxblock.py:63 msgid "Upload scorm file" -msgstr "" +msgstr "Завантажити SCORM файл" #: edx_xblock_scorm/scormxblock/scormxblock.py:67 msgid "Path to the index page in scorm file" @@ -35,54 +35,54 @@ msgstr "" #: edx_xblock_scorm/scormxblock/scormxblock.py:99 msgid "Scored" -msgstr "" +msgstr "Оцінено" #: edx_xblock_scorm/scormxblock/scormxblock.py:100 msgid "" "Select False if this component will not receive a numerical score from the " "Scorm" -msgstr "" +msgstr "Оберіть \"False\", щоб завдання не оцінювалося" #: edx_xblock_scorm/scormxblock/scormxblock.py:109 msgid "Display Width (px)" -msgstr "" +msgstr "Ширина (px)" #: edx_xblock_scorm/scormxblock/scormxblock.py:110 msgid "Width of iframe, if empty, the default 100" -msgstr "" +msgstr "Ширина iframe (вікна)" #: edx_xblock_scorm/scormxblock/scormxblock.py:114 msgid "Display Height (px)" -msgstr "" +msgstr "Висота (px)" #: edx_xblock_scorm/scormxblock/scormxblock.py:115 msgid "Height of iframe" -msgstr "" +msgstr "Висота iframe (вікна)" #: edx_xblock_scorm/scormxblock/static/html/author_view.html:3 msgid "Look in LMS" -msgstr "" +msgstr "Шукати на LMS" #: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:8 msgid "points" -msgstr "" +msgstr "Бали" #: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:16 msgid "Full screen" -msgstr "" +msgstr "Повноекранний режим" #: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:19 msgid "Exit full screen" -msgstr "" +msgstr "Вийти з повноекранного режиму " #: edx_xblock_scorm/scormxblock/static/html/studio.html:18 msgid "Currently:" -msgstr "" +msgstr "Статус:" #: edx_xblock_scorm/scormxblock/static/html/studio.html:51 msgid "Save" -msgstr "" +msgstr "Зберегти" #: edx_xblock_scorm/scormxblock/static/html/studio.html:54 msgid "Cancel" -msgstr "" +msgstr "Відмінити" From 7fbe54eeddf4a2f7773ccf215650385fcb868986 Mon Sep 17 00:00:00 2001 From: Vladislav Zhuravel Date: Wed, 4 May 2022 23:44:20 +0300 Subject: [PATCH 09/13] feat: [MON-2266] gettext wrap completion_status --- scormxblock/scormxblock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index afa81b1d..01a090d7 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -334,10 +334,11 @@ def set_fields_xblock(self): self.scorm_file = os.path.join(self.folder_path, self.path_index_page) def get_completion_status(self): + _ = self.runtime.service(self, 'i18n').ugettext completion_status = self.lesson_status if self.version_scorm == 'SCORM_2004' and self.success_status != 'unknown': completion_status = self.success_status - return completion_status + return _(completion_status) def _file_storage_path(self): """ From 1cb52640e124e67e134bef6a3c30239d38b26017 Mon Sep 17 00:00:00 2001 From: Vladislav Zhuravel Date: Fri, 6 May 2022 09:15:04 +0300 Subject: [PATCH 10/13] feat: [MON-2266] add missing translates and change old ones --- .../translations/uk/LC_MESSAGES/text.mo | Bin 1479 -> 1724 bytes .../translations/uk/LC_MESSAGES/text.po | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.mo b/scormxblock/translations/uk/LC_MESSAGES/text.mo index 3f0d8453c13b6c7c2dacf39a9e373eff87950ae2..3e7fcea03947bacae6b37ddf42e5743fa928e476 100644 GIT binary patch delta 681 zcmZ9}IZFdU6u|LWjT(;_<3Yd!7bHzkh!8=r6Rd2s(8|Umh2Rm0S_l$BLY{wZtp7-Mw43OVK&dr((lQ|Om7{M2e<2wd1*jT?f zf?CL+?+8Y@JdRqxG^gr?3#jKT;{ay-IEP8%Tb#sKjB&pSHyLA-n7}?czWM6~$B3_y)D`B2u3T(e;aI(*l263zLYxxo*;nbQ62L%PG!qzW2$& zYrLr8qnL0#^u|_3{dM{)yjDE6rmX(e?alN?YA0pZxBjN-Fn;(RKo@oF8 delta 435 zcmXZYy-or_5Ww+SIOXF+@C+eQ3KD~b5DKI)v9YJ&4SWhhNrXl_DX5Hvi8Yd3W8wwq zsEB)swY-3piKU7E*>jTJ-|pOg%xzRy`@_$`n+kD2tJ0on6M8yVyY8Fu)o07$D?>0--OrR(&WWX K?A*TDSNQ|3;Vkd~ diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.po b/scormxblock/translations/uk/LC_MESSAGES/text.po index cf20d563..2537329a 100644 --- a/scormxblock/translations/uk/LC_MESSAGES/text.po +++ b/scormxblock/translations/uk/LC_MESSAGES/text.po @@ -49,7 +49,7 @@ msgstr "Ширина (px)" #: edx_xblock_scorm/scormxblock/scormxblock.py:110 msgid "Width of iframe, if empty, the default 100" -msgstr "Ширина iframe (вікна)" +msgstr "Ширина iframe (вікна), якщо поле порожнє - ширина за замовчуванням 100%" #: edx_xblock_scorm/scormxblock/scormxblock.py:114 msgid "Display Height (px)" @@ -65,7 +65,7 @@ msgstr "Шукати на LMS" #: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:8 msgid "points" -msgstr "Бали" +msgstr "бали" #: edx_xblock_scorm/scormxblock/static/html/scormxblock.html:16 msgid "Full screen" @@ -77,7 +77,7 @@ msgstr "Вийти з повноекранного режиму " #: edx_xblock_scorm/scormxblock/static/html/studio.html:18 msgid "Currently:" -msgstr "Статус:" +msgstr "Зараз:" #: edx_xblock_scorm/scormxblock/static/html/studio.html:51 msgid "Save" @@ -86,3 +86,12 @@ msgstr "Зберегти" #: edx_xblock_scorm/scormxblock/static/html/studio.html:54 msgid "Cancel" msgstr "Відмінити" + +msgid "incomplete" +msgstr "не закінчено" + +msgid "complete" +msgstr "закінчено" + +msgid "passed" +msgstr "закінчено" From b11922116ba47e35aaa4d77d609b6acbb59819ce Mon Sep 17 00:00:00 2001 From: Taras Lytvynenko <69678257+Inferato@users.noreply.github.com> Date: Tue, 10 May 2022 14:46:55 +0300 Subject: [PATCH 11/13] feat: [MON-2319] Translations for messages --- .../translations/uk/LC_MESSAGES/text.mo | Bin 1724 -> 2903 bytes .../translations/uk/LC_MESSAGES/text.po | 37 ++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.mo b/scormxblock/translations/uk/LC_MESSAGES/text.mo index 3e7fcea03947bacae6b37ddf42e5743fa928e476..57517b44743489babe74e64fa328c4e642c924be 100644 GIT binary patch literal 2903 zcmchX-)|IE6vwZEzgGN(KS3T&h$JALvQ&&@0n@eJLc?y`bhjAw0f*hY+bgp(o0-`{ z(=?W1s2XF9A_Noh#b1vLZDC8>B_=)+b0@}E6BGRhjEQf?@0rDDR8`qa|_OYaBjnS<33z)ynDY84}vBb1BbzO_%V0`jTXS_jvTtpaI={y8Sc zYaJX5l7%&K9j)UD+-$-L8N)uj(0su?Y#p2VrpvVR(h^-&*Oj(!jV465@+y`&N{6H> z4g0jca&pJ=vp%ydmltoUfl}BZ#P_?8;om4&*tCmGx-j&i8JvZGq zqc}xUMVGy&9p^YHo6?!A$eJTkWb=+I3nD8mnfIy3v^)vhC_>&TR~#FO(+LItw&Rm4 z^HPmSGRdx%rK|EL$At?jx=tC}B(cY`$f}affh=Vo7dg&1@7O;3%9}Phwlzw}B#kI} zLKes@`qHH_X}gYPjq^4W=2h2|?W^~~O}N#}=cVVVW0oY}p=clcSkiQD>3Z!n<{|!3 zWsei&8gpeyc?dDyMy6e8+H;st$S4v!;&e#)?P926Ic6cOtV~4(nWJ_r3)#jZ$#TUX zZ3~Mo$f8-beA*q43zp52z7$bj$}S0IFF&l9o(I7zjD~Eue_tw=Ha*|Sxu)$|rti23>KRI>sV|vH(f(8>*_-Z6c6Se^vf0;RYL}{J zNg6q6mJ^gt_8lDJl=r98otA2M+|r5-QqA;cQY-cCZjZ;hqIouQqZQ=s%ai^teA&w0 zNBLnBqw60S%Jmp~SL`^gqI8WEhM|C|NYLJ6%8v>Cd2mk8>P1|abUm2Vb)nDeS}>t& z`f38N@$i&x;6*L^ALy_7jlPUEP{YkEu1k6;m=^j+SWoCWmM!bZeM4UgJzjz5v%zHW zNpLEdrqE1Z*9&+$8GL{!6P!pGRL^rR2thDtAj%T1bNX872GOvZ(G9|KPG3R9#o#mv z{mq)Dbv=KR;p&>AUlVQDm!ZHl&S(e92u|Z3NpS{T3;(eo%OyP*2Aqj>x)_`ZWt-!4 zguWQP56*Fcv^D&lZEd6ABP4a{HnG_89HHjWNAM{t3MCq>j9^_6g9WLx%4`&z7y5ft zJ5R{10lg=&z?IMd-ZhD&ufr))maQEI_KIi(S?svt05MqW=W%LmhP(S7JW)Shz+PU$Kr~n8Vy?TG&Nxq?);c z9>wdZ4Qz0#7Cb;br-6$YXZbz)}7FKZ<4^eN>!b!ZrUA)5@4)6+x zsD&@-%EMdK{1LVB9@3wb82Vxw+TeeT^Wo~vb(0p-P3%dE)0}XA^5l>N7Zn~w%4wR+ zd1n1}`YVz!$JWp+*xU2X@U(p%wSrC{Q6r9nGl|;GuoIh2XTA6ATv%o&|6+;-{||#G BFaZDn diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.po b/scormxblock/translations/uk/LC_MESSAGES/text.po index 2537329a..ac7877bc 100644 --- a/scormxblock/translations/uk/LC_MESSAGES/text.po +++ b/scormxblock/translations/uk/LC_MESSAGES/text.po @@ -95,3 +95,40 @@ msgstr "закінчено" msgid "passed" msgstr "закінчено" + +#: common/lib/xmodule/xmodule/x_module.py +#, python-brace-format +msgid "" +"{display_name} is only accessible to enrolled learners. " +"{sign_in} or {register}, and enroll in this course to view it." +msgstr "" +"{display_name} доступне для перегляду та виконання після {register}/{sign_in} " +"на платформі." + +#: common/lib/xmodule/xmodule/x_module.py +#, python-brace-format +msgid "" +"This content is only accessible to enrolled learners. " +"{sign_in} or {register}, and enroll in this course to view it." +msgstr "" +"Цей контент доступний для перегляду та виконання після {register}/{sign_in} " +"на платформі." + +msgid "Sign in link" +msgstr "авторизації" + +msgid "register link" +msgstr "реєстрації" + +msgid "" +"{display_name} can only be viewed after {enroll} in a course." +msgstr "" +"{display_name} доступне для перегляду та виконання після {enroll} на курсі." + +msgid "" +"This content can only be viewed after {enroll} in a course." +msgstr "" +"Цей контент доступний для перегляду та виконання після {enroll} на курсі." + +msgid "enrolling" +msgstr "реєстрації" From 594da1b6234cd5651c1be0947e7fc6b4fdbeaf98 Mon Sep 17 00:00:00 2001 From: Taras Lytvynenko <69678257+Inferato@users.noreply.github.com> Date: Wed, 11 May 2022 12:52:04 +0300 Subject: [PATCH 12/13] feat: [MON-2319] Translations for messages --- .../translations/uk/LC_MESSAGES/text.mo | Bin 2903 -> 1765 bytes .../translations/uk/LC_MESSAGES/text.po | 37 ------------------ 2 files changed, 37 deletions(-) diff --git a/scormxblock/translations/uk/LC_MESSAGES/text.mo b/scormxblock/translations/uk/LC_MESSAGES/text.mo index 57517b44743489babe74e64fa328c4e642c924be..5578f5eca4d0693a7cbdcdd135e8c63b9fe9302c 100644 GIT binary patch delta 515 zcmXxhze~eF6u|Mwk2r`=Silz? z$4@NcHB_=)+b0@}E6BGRhjEQf?@0rDDR8`qa|_OYaBjnS<33z)ynDY84}vBb1BbzO_%V0`jTXS_jvTtpaI={y8Sc zYaJX5l7%&K9j)UD+-$-L8N)uj(0su?Y#p2VrpvVR(h^-&*Oj(!jV465@+y`&N{6H> z4g0jca&pJ=vp%ydmltoUfl}BZ#P_?8;om4&*tCmGx-j&i8JvZGq zqc}xUMVGy&9p^YHo6?!A$eJTkWb=+I3nD8mnfIy3v^)vhC_>&TR~#FO(+LItw&Rm4 z^HPmSGRdx%rK|EL$At?jx=tC}B(cY`$f}affh=Vo7dg&1@7O;3%9}Phwlzw}B#kI} zLKes@`qHH_X}gYPjq^4W=2h2|?W^~~O}N#}=cVVVW0oY}p=clcSkiQD>3Z!n<{|!3 zWsei&8gpeyc?dDyMy6e8+H;st$S4v!;&e#)?P926Ic6cOtV~4(nWJ_r3)#jZ$#TUX zZ3~Mo$f8-beA*q43zp52z7$bj$}S0IFF&l9o(I7zjD~Eue_tw=Ha*|Sxu)$|rti23>KRI>sV|vH(f(8>*_-Z6c6Se^vf0;RYL}{J zNg6q6mJ^gt_8lDJl=r98otA2M+|r5-QqA;cQY-cCZjZ;hqIouQqZQ=s%ai^teA&w0 zNBLnBqw60S%Jmp~SL`^gqI8WEhM|C|NYLJ6%8v>Cd2mk8>P1|abUm2Vb)nDeS}>t& z`f38N@$i&x;6*L^ALy_7jlPUEP{YkEu1k6;m=^j+SWoCWmM!bZeM4UgJzjz5v%zHW zNpLEdrqE1Z*9&+$8GL{!6P!pGRL^rR2thDtAj%T1bNX872GOvZ(G9|KPG3R9#o#mv z{mq)Dbv=KR;p&>AUlVQDm!ZHl&S(e92u|Z3NpS{T3;(eo%OyP*2Aqj>x)_`ZWt-!4 zguWQP56*Fcv^D&lZEd6ABP4a{HnG_89HHjWNAM{t3MCq>j9^_6g9WLx%4`&z7y5ft zJ5R{10lg=&z?IMd-ZhD&ufr) Date: Mon, 6 Jun 2022 15:48:41 +0300 Subject: [PATCH 13/13] fix: [MON-2416] remove type converison to unicode Python2 --- scormxblock/scormxblock.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scormxblock/scormxblock.py b/scormxblock/scormxblock.py index 01a090d7..06867ecc 100644 --- a/scormxblock/scormxblock.py +++ b/scormxblock/scormxblock.py @@ -36,8 +36,6 @@ class FileIter(object): def __init__(self, _file, _type='application/octet-stream'): self._file = _file self.wrapper = lambda d: d - if _type.startswith('text'): - self.wrapper = lambda d: unicode(d, 'utf-8', 'replace') def __iter__(self): try: