From 555ebc80a5b1809847ec86bc94e9bdc407342299 Mon Sep 17 00:00:00 2001 From: monteri Date: Tue, 21 May 2024 15:39:07 +0200 Subject: [PATCH] feat: offline mode app --- .../mobile_api/offline_mode/__init__.py | 3 - .../mobile_api/offline_mode/signals.py | 20 --- .../tests/test_html_manipulation.py | 0 .../offline_mode/tests/test_signals.py | 0 .../offline_mode/tests/test_tasks.py | 0 .../tests => offline_mode}/__init__.py | 0 lms/djangoapps/offline_mode/handlers.py | 14 ++ .../{mobile_api => }/offline_mode/tasks.py | 9 +- .../utils => offline_mode/tests}/__init__.py | 0 .../tests/test_assets_management.py | 140 ++++++++++++++++++ .../tests/test_html_manipulation.py | 72 +++++++++ .../offline_mode/tests/test_signals.py | 33 +++++ .../offline_mode/tests/test_tasks.py | 71 +++++++++ .../offline_mode/tests/test_xblock_helpers.py | 0 .../offline_mode/tests/test_zip_management.py | 0 lms/djangoapps/offline_mode/toggles.py | 17 +++ .../utils/__init__.py} | 0 .../offline_mode/utils/assets_management.py | 29 ++++ .../offline_mode/utils/file_management.py | 6 + .../offline_mode/utils/html_manipulator.py | 0 .../offline_mode/utils/xblock_helpers.py | 27 ++-- .../offline_mode/utils/zip_management.py | 0 22 files changed, 400 insertions(+), 41 deletions(-) delete mode 100644 lms/djangoapps/mobile_api/offline_mode/__init__.py delete mode 100644 lms/djangoapps/mobile_api/offline_mode/signals.py delete mode 100644 lms/djangoapps/mobile_api/offline_mode/tests/test_html_manipulation.py delete mode 100644 lms/djangoapps/mobile_api/offline_mode/tests/test_signals.py delete mode 100644 lms/djangoapps/mobile_api/offline_mode/tests/test_tasks.py rename lms/djangoapps/{mobile_api/offline_mode/tests => offline_mode}/__init__.py (100%) create mode 100644 lms/djangoapps/offline_mode/handlers.py rename lms/djangoapps/{mobile_api => }/offline_mode/tasks.py (68%) rename lms/djangoapps/{mobile_api/offline_mode/utils => offline_mode/tests}/__init__.py (100%) create mode 100644 lms/djangoapps/offline_mode/tests/test_assets_management.py create mode 100644 lms/djangoapps/offline_mode/tests/test_html_manipulation.py create mode 100644 lms/djangoapps/offline_mode/tests/test_signals.py create mode 100644 lms/djangoapps/offline_mode/tests/test_tasks.py rename lms/djangoapps/{mobile_api => }/offline_mode/tests/test_xblock_helpers.py (100%) rename lms/djangoapps/{mobile_api => }/offline_mode/tests/test_zip_management.py (100%) create mode 100644 lms/djangoapps/offline_mode/toggles.py rename lms/djangoapps/{mobile_api/offline_mode/tests/test_assets_management.py => offline_mode/utils/__init__.py} (100%) rename lms/djangoapps/{mobile_api => }/offline_mode/utils/assets_management.py (61%) rename lms/djangoapps/{mobile_api => }/offline_mode/utils/file_management.py (61%) rename lms/djangoapps/{mobile_api => }/offline_mode/utils/html_manipulator.py (100%) rename lms/djangoapps/{mobile_api => }/offline_mode/utils/xblock_helpers.py (91%) rename lms/djangoapps/{mobile_api => }/offline_mode/utils/zip_management.py (100%) diff --git a/lms/djangoapps/mobile_api/offline_mode/__init__.py b/lms/djangoapps/mobile_api/offline_mode/__init__.py deleted file mode 100644 index de13de8df799..000000000000 --- a/lms/djangoapps/mobile_api/offline_mode/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Offline mode -""" diff --git a/lms/djangoapps/mobile_api/offline_mode/signals.py b/lms/djangoapps/mobile_api/offline_mode/signals.py deleted file mode 100644 index 53cf4679c5b8..000000000000 --- a/lms/djangoapps/mobile_api/offline_mode/signals.py +++ /dev/null @@ -1,20 +0,0 @@ -import six -from django.dispatch import receiver -from openedx_events.content_authoring.signals import ( - XBLOCK_CREATED, - XBLOCK_DELETED, - XBLOCK_DUPLICATED, - XBLOCK_UPDATED, - XBLOCK_PUBLISHED, -) - -from xmodule.modulestore.django import SignalHandler - -from .tasks import generate_course_media - - -@receiver([XBLOCK_PUBLISHED]) -def hello_world(**kwargs): - import pdb; pdb.set_trace() - pass - # generate_course_media.delay(six.text_type(course_key)) diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/test_html_manipulation.py b/lms/djangoapps/mobile_api/offline_mode/tests/test_html_manipulation.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/test_signals.py b/lms/djangoapps/mobile_api/offline_mode/tests/test_signals.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/test_tasks.py b/lms/djangoapps/mobile_api/offline_mode/tests/test_tasks.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/__init__.py b/lms/djangoapps/offline_mode/__init__.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/tests/__init__.py rename to lms/djangoapps/offline_mode/__init__.py diff --git a/lms/djangoapps/offline_mode/handlers.py b/lms/djangoapps/offline_mode/handlers.py new file mode 100644 index 000000000000..06eed7d28c75 --- /dev/null +++ b/lms/djangoapps/offline_mode/handlers.py @@ -0,0 +1,14 @@ +import six +from django.dispatch import receiver +from openedx_events.content_authoring.signals import XBLOCK_PUBLISHED + +from xmodule.modulestore.django import SignalHandler + +from .tasks import generate_course_media + + +@receiver([XBLOCK_PUBLISHED]) +def listen_course_publish(**kwargs): + if USER_TOURS_DISABLED.is_disabled(): + return + generate_course_media.delay(six.text_type(course_key)) diff --git a/lms/djangoapps/mobile_api/offline_mode/tasks.py b/lms/djangoapps/offline_mode/tasks.py similarity index 68% rename from lms/djangoapps/mobile_api/offline_mode/tasks.py rename to lms/djangoapps/offline_mode/tasks.py index c7abdb62522b..4fdf01d75660 100644 --- a/lms/djangoapps/mobile_api/offline_mode/tasks.py +++ b/lms/djangoapps/offline_mode/tasks.py @@ -2,7 +2,12 @@ from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore -from .utils.xblock_helpers import generate_offline_content, xblock_view_handler, generate_request_with_service_user +from .utils.xblock_helpers import ( + generate_offline_content, + xblock_view_handler, + generate_request_with_service_user, + is_offline_supported, +) @shared_task @@ -10,5 +15,7 @@ def generate_course_media(course_id): request = generate_request_with_service_user() course_key = CourseKey.from_string(course_id) for xblock in modulestore().get_items(course_key, qualifiers={'category': 'problem'}): + if is_offline_supported(xblock): + continue html_data = xblock_view_handler(request, xblock) generate_offline_content(xblock, html_data) diff --git a/lms/djangoapps/mobile_api/offline_mode/utils/__init__.py b/lms/djangoapps/offline_mode/tests/__init__.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/utils/__init__.py rename to lms/djangoapps/offline_mode/tests/__init__.py diff --git a/lms/djangoapps/offline_mode/tests/test_assets_management.py b/lms/djangoapps/offline_mode/tests/test_assets_management.py new file mode 100644 index 000000000000..b3450fbc1ada --- /dev/null +++ b/lms/djangoapps/offline_mode/tests/test_assets_management.py @@ -0,0 +1,140 @@ +import os + +from unittest import TestCase +from unittest.mock import patch, MagicMock +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage + +from xmodule.assetstore.assetmgr import AssetManager +from xmodule.contentstore.content import StaticContent +from xmodule.exceptions import NotFoundError +from xmodule.modulestore.exceptions import ItemNotFoundError + +from .file_management import save_asset_file, remove_old_files, base_storage_path + +class TestFileManagement(TestCase): + + @patch('file_management.get_static_file_path') + @patch('file_management.read_static_file') + @patch('xmodule.contentstore.content.StaticContent.get_asset_key_from_path') + @patch('xmodule.assetstore.assetmgr.AssetManager.find') + @patch('django.core.files.storage.default_storage.save') + def test_save_asset_file_with_static_file( + self, + mock_save, + mock_find, + mock_get_asset_key, + mock_read_static_file, + mock_get_static_file_path + ): + xblock = MagicMock() + xblock.location.course_key = 'course-v1:edX+DemoX+Demo_Course' + path = 'path/to/asset' + filename = 'static/file/path.txt' + content = b'some content' + + mock_get_static_file_path.return_value = 'static/file/path.txt' + mock_read_static_file.return_value = content + + save_asset_file(xblock, path, filename) + + mock_get_static_file_path.assert_called_with(filename) + mock_read_static_file.assert_called_with('static/file/path.txt') + mock_save.assert_called_with( + f'{xblock.location.org}/{xblock.location.course}' + f'/{xblock.location.block_type}/{xblock.location.block_id}/assets/{filename}', + ContentFile(content) + ) + + @patch('xmodule.contentstore.content.StaticContent.get_asset_key_from_path') + @patch('xmodule.assetstore.assetmgr.AssetManager.find') + @patch('django.core.files.storage.default_storage.save') + def test_save_asset_file_with_asset_manager(self, mock_save, mock_find, mock_get_asset_key): + xblock = MagicMock() + xblock.location.course_key = 'course-v1:edX+DemoX+Demo_Course' + path = 'path/to/asset' + filename = 'asset.txt' + content = b'some content' + + mock_get_asset_key.return_value = 'asset_key' + mock_find.return_value.data = content + + save_asset_file(xblock, path, filename) + + mock_get_asset_key.assert_called_with(xblock.location.course_key, path) + mock_find.assert_called_with('asset_key') + mock_save.assert_called_with( + f'{xblock.location.org}/{xblock.location.course}' + f'/{xblock.location.block_type}/{xblock.location.block_id}/assets/{filename}', + ContentFile(content) + ) + + @patch('xmodule.contentstore.content.StaticContent.get_asset_key_from_path') + @patch('xmodule.assetstore.assetmgr.AssetManager.find') + @patch('file_management.get_static_file_path') + @patch('file_management.read_static_file') + @patch('django.core.files.storage.default_storage.save') + def test_save_asset_file_not_found_error( + self, + mock_save, + mock_read_static_file, + mock_get_static_file_path, + mock_find, + mock_get_asset_key + ): + xblock = MagicMock() + xblock.location.course_key = 'course-v1:edX+DemoX+Demo_Course' + path = 'path/to/asset' + filename = 'asset.txt' + + mock_get_asset_key.side_effect = ItemNotFoundError + mock_find.side_effect = NotFoundError + + save_asset_file(xblock, path, filename) + + mock_save.assert_not_called() + + @patch('django.core.files.storage.default_storage.listdir') + @patch('django.core.files.storage.default_storage.delete') + def test_remove_old_files(self, mock_delete, mock_listdir): + base_path = 'base/path/' + files = ['file1.txt', 'file2.txt'] + assets_files = ['asset1.txt', 'asset2.txt'] + + mock_listdir.side_effect = [ + ([], files), # for base_path + ([], assets_files), # for base_path + 'assets/' + ] + + remove_old_files(base_path) + + expected_delete_calls = [ + patch('django.core.files.storage.default_storage.delete').call(base_path + 'file1.txt'), + patch('django.core.files.storage.default_storage.delete').call(base_path + 'file2.txt'), + patch('django.core.files.storage.default_storage.delete').call(base_path + 'assets/' + 'asset1.txt'), + patch('django.core.files.storage.default_storage.delete').call(base_path + 'assets/' + 'asset2.txt'), + ] + + mock_delete.assert_has_calls(expected_delete_calls, any_order=True) + + @patch('django.core.files.storage.default_storage.listdir') + @patch('django.core.files.storage.default_storage.delete') + def test_remove_old_files_os_error(self, mock_delete, mock_listdir): + base_path = 'base/path/' + + mock_listdir.side_effect = OSError + + remove_old_files(base_path) + + mock_delete.assert_not_called() + + def test_base_storage_path(self): + xblock = MagicMock() + xblock.location.org = 'edX' + xblock.location.course = 'DemoX' + xblock.location.block_type = 'block' + xblock.location.block_id = 'block_id' + + expected_path = 'edX/DemoX/block/block_id/' + + self.assertEqual(base_storage_path(xblock), expected_path) diff --git a/lms/djangoapps/offline_mode/tests/test_html_manipulation.py b/lms/djangoapps/offline_mode/tests/test_html_manipulation.py new file mode 100644 index 000000000000..2ec8b692a26a --- /dev/null +++ b/lms/djangoapps/offline_mode/tests/test_html_manipulation.py @@ -0,0 +1,72 @@ +import unittest +from unittest.mock import Mock, patch +from bs4 import BeautifulSoup + +from .html_manipulator import HtmlManipulator + + +class HtmlManipulatorTest(unittest.TestCase): + + def setUp(self): + self.xblock = Mock() + self.html_data = ''' + + + + + + + + + + ''' + self.manipulator = HtmlManipulator(self.xblock, self.html_data) + + @patch('html_manipulator.save_asset_file') + def test_replace_mathjax_link(self, mock_save_asset_file): + updated_html = self.manipulator._replace_mathjax_link() + self.assertNotIn('https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js', updated_html) + self.assertIn('src="/static/mathjax/MathJax.js"', updated_html) + + @patch('html_manipulator.save_asset_file') + def test_replace_static_links(self, mock_save_asset_file): + updated_html = self.manipulator._replace_static_links() + self.assertIn('assets/img/sample.png', updated_html) + mock_save_asset_file.assert_called_with(self.xblock, '/static/img/sample.png', 'img/sample.png') + + def test_replace_iframe(self): + soup = BeautifulSoup(self.html_data, 'html.parser') + self.manipulator._replace_iframe(soup) + self.assertEqual(len(soup.find_all('iframe')), 0) + self.assertEqual(len(soup.find_all('a', href='https://example.com/video')), 1) + + def test_add_js_bridge(self): + soup = BeautifulSoup(self.html_data, 'html.parser') + self.manipulator._add_js_bridge(soup) + script_tag = soup.find('script', string=lambda text: 'sendMessageToiOS' in text if text else False) + self.assertIsNotNone(script_tag) + self.assertIn('sendMessageToAndroid', script_tag.string) + + @patch('html_manipulator.save_asset_file') + def test_process_html(self, mock_save_asset_file): + final_html = self.manipulator.process_html() + soup = BeautifulSoup(final_html, 'html.parser') + + # Check MathJax link replacement + mathjax_script = soup.find('script', src='/static/mathjax/MathJax.js') + self.assertIsNotNone(mathjax_script) + + # Check iframe replacement + iframes = soup.find_all('iframe') + self.assertEqual(len(iframes), 0) + anchors = soup.find_all('a', href='https://example.com/video') + self.assertEqual(len(anchors), 1) + + # Check static link replacement + img_tag = soup.find('img', src='assets/img/sample.png') + self.assertIsNotNone(img_tag) + mock_save_asset_file.assert_called_with(self.xblock, '/static/img/sample.png', 'img/sample.png') + + # Check JS bridge script + script_tag = soup.find('script', string=lambda text: 'sendMessageToiOS' in text if text else False) + self.assertIsNotNone(script_tag) diff --git a/lms/djangoapps/offline_mode/tests/test_signals.py b/lms/djangoapps/offline_mode/tests/test_signals.py new file mode 100644 index 000000000000..d2f39ebc9ef5 --- /dev/null +++ b/lms/djangoapps/offline_mode/tests/test_signals.py @@ -0,0 +1,33 @@ +import unittest +from unittest.mock import patch, Mock +from django.test import TestCase +from django.dispatch import Signal + +from lms.djangoapps.offline_mode.handlers import listen_course_publish +from lms.djangoapps.offline_mode.tasks import generate_course_media + +# Mocking the XBLOCK_PUBLISHED signal +XBLOCK_PUBLISHED = Signal(providing_args=["course_key"]) + +class ListenCoursePublishSignalTest(TestCase): + + def setUp(self): + self.course_key = 'course-v1:edX+DemoX+Demo_Course' + + @patch('myapp.signals.generate_course_media.delay') + @patch('myapp.signals.USER_TOURS_DISABLED.is_disabled', return_value=False) + def test_listen_course_publish_signal_handler(self, mock_is_disabled, mock_generate_course_media): + # Simulate sending the signal + XBLOCK_PUBLISHED.send(sender=None, course_key=self.course_key) + + # Check if the generate_course_media task was called with the correct arguments + mock_generate_course_media.assert_called_once_with(self.course_key) + + @patch('myapp.signals.generate_course_media.delay') + @patch('myapp.signals.USER_TOURS_DISABLED.is_disabled', return_value=True) + def test_listen_course_publish_signal_handler_disabled(self, mock_is_disabled, mock_generate_course_media): + # Simulate sending the signal + XBLOCK_PUBLISHED.send(sender=None, course_key=self.course_key) + + # Check that the generate_course_media task was not called since the feature is disabled + mock_generate_course_media.assert_not_called() diff --git a/lms/djangoapps/offline_mode/tests/test_tasks.py b/lms/djangoapps/offline_mode/tests/test_tasks.py new file mode 100644 index 000000000000..feb1c0ee5592 --- /dev/null +++ b/lms/djangoapps/offline_mode/tests/test_tasks.py @@ -0,0 +1,71 @@ +# lint-amnesty, pylint: disable=missing-module-docstring +from unittest import mock +from unittest.mock import patch + +import ddt +from django.test import TestCase +from celery import shared_task + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order +from opaque_keys.edx.keys import CourseKey + +from lms.djangoapps.offline_mode.tasks import generate_course_media +from lms.djangoapps.offline_mode.utils.xblock_helpers import ( + generate_offline_content, + xblock_view_handler, + generate_request_with_service_user, + is_offline_supported, +) + + +@ddt.ddt +class TestGenerateCourseMediaTask(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring + @patch('lms.djangoapps.offline_mode.tasks.generate_request_with_service_user') + @patch('lms.djangoapps.offline_mode.tasks.modulestore') + @patch('lms.djangoapps.offline_mode.tasks.xblock_view_handler') + @patch('lms.djangoapps.offline_mode.tasks.generate_offline_content') + @patch('lms.djangoapps.offline_mode.tasks.is_offline_supported') + def test_generate_course_media(self, mock_is_offline_supported, mock_generate_offline_content, mock_xblock_view_handler, mock_modulestore, mock_generate_request_with_service_user): + # Arrange + course_id = 'course-v1:edX+DemoX+Demo_Course' + course_key = CourseKey.from_string(course_id) + request = mock.Mock() + mock_generate_request_with_service_user.return_value = request + mock_xblock = mock.Mock(category='problem') + mock_modulestore().get_items.return_value = [mock_xblock] + mock_is_offline_supported.return_value = False + html_data = '
Sample HTML
' + mock_xblock_view_handler.return_value = html_data + + # Act + generate_course_media(course_id) + + # Assert + mock_generate_request_with_service_user.assert_called_once() + mock_modulestore().get_items.assert_called_once_with(course_key, qualifiers={'category': 'problem'}) + mock_is_offline_supported.assert_called_once_with(mock_xblock) + mock_xblock_view_handler.assert_called_once_with(request, mock_xblock) + mock_generate_offline_content.assert_called_once_with(mock_xblock, html_data) + + @patch('lms.djangoapps.offline_mode.tasks.generate_request_with_service_user') + @patch('lms.djangoapps.offline_mode.tasks.modulestore') + @patch('lms.djangoapps.offline_mode.tasks.is_offline_supported') + def test_generate_course_media_offline_supported(self, mock_is_offline_supported, mock_modulestore, mock_generate_request_with_service_user): + # Arrange + course_id = 'course-v1:edX+DemoX+Demo_Course' + course_key = CourseKey.from_string(course_id) + request = mock.Mock() + mock_generate_request_with_service_user.return_value = request + mock_xblock = mock.Mock(category='problem') + mock_modulestore().get_items.return_value = [mock_xblock] + mock_is_offline_supported.return_value = True + + # Act + generate_course_media(course_id) + + # Assert + mock_generate_request_with_service_user.assert_called_once() + mock_modulestore().get_items.assert_called_once_with(course_key, qualifiers={'category': 'problem'}) + mock_is_offline_supported.assert_called_once_with(mock_xblock) + self.assertFalse(mock_xblock_view_handler.called) + self.assertFalse(mock_generate_offline_content.called) diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/test_xblock_helpers.py b/lms/djangoapps/offline_mode/tests/test_xblock_helpers.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/tests/test_xblock_helpers.py rename to lms/djangoapps/offline_mode/tests/test_xblock_helpers.py diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/test_zip_management.py b/lms/djangoapps/offline_mode/tests/test_zip_management.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/tests/test_zip_management.py rename to lms/djangoapps/offline_mode/tests/test_zip_management.py diff --git a/lms/djangoapps/offline_mode/toggles.py b/lms/djangoapps/offline_mode/toggles.py new file mode 100644 index 000000000000..bbb09bead92c --- /dev/null +++ b/lms/djangoapps/offline_mode/toggles.py @@ -0,0 +1,17 @@ +""" +Toggles for the Offline Mode Experience. +""" + +from edx_toggles.toggles import WaffleFlag + +# .. toggle_name: offline_node.media_generation_enabled +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: This flag enables media generation for offline mode. +# .. toggle_warnings: None +# .. toggle_use_cases: opt_out +# .. toggle_creation_date: 2024-05-20 +# .. toggle_target_removal_date: None +MEDIA_GENERATION_ENABLED = WaffleFlag( + 'offline_node.media_generation_enabled', module_name=__name__, log_prefix='offline_mode' +) diff --git a/lms/djangoapps/mobile_api/offline_mode/tests/test_assets_management.py b/lms/djangoapps/offline_mode/utils/__init__.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/tests/test_assets_management.py rename to lms/djangoapps/offline_mode/utils/__init__.py diff --git a/lms/djangoapps/mobile_api/offline_mode/utils/assets_management.py b/lms/djangoapps/offline_mode/utils/assets_management.py similarity index 61% rename from lms/djangoapps/mobile_api/offline_mode/utils/assets_management.py rename to lms/djangoapps/offline_mode/utils/assets_management.py index f8e28128a726..7edb34ff4ff3 100644 --- a/lms/djangoapps/mobile_api/offline_mode/utils/assets_management.py +++ b/lms/djangoapps/offline_mode/utils/assets_management.py @@ -11,6 +11,17 @@ def save_asset_file(xblock, path, filename): + """ + Saves an asset file to the default storage. + + If the filename contains a '/', it reads the static file directly from the file system. + Otherwise, it fetches the asset from the AssetManager. + + Args: + xblock (XBlock): The XBlock instance that provides context for the file. + path (str): The path where the asset is located. + filename (str): The name of the file to be saved. + """ try: if '/' in filename: static_path = get_static_file_path(filename) @@ -26,6 +37,12 @@ def save_asset_file(xblock, path, filename): def remove_old_files(base_path): + """ + Removes old files from the specified base path and its 'assets/' subdirectory. + + Args: + base_path (str): The base path from which to delete files. + """ try: directories, files = default_storage.listdir(base_path) except OSError: @@ -44,4 +61,16 @@ def remove_old_files(base_path): def base_storage_path(xblock): + """ + Generates the base storage path for the given XBlock. + + The path is constructed based on the XBlock's location, which includes the organization, + course, block type, and block ID. + + Args: + xblock (XBlock): The XBlock instance for which to generate the storage path. + + Returns: + str: The constructed base storage path. + """ return '{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/'.format(loc=xblock.location) diff --git a/lms/djangoapps/mobile_api/offline_mode/utils/file_management.py b/lms/djangoapps/offline_mode/utils/file_management.py similarity index 61% rename from lms/djangoapps/mobile_api/offline_mode/utils/file_management.py rename to lms/djangoapps/offline_mode/utils/file_management.py index 170889d96b9e..829fcf18211b 100644 --- a/lms/djangoapps/mobile_api/offline_mode/utils/file_management.py +++ b/lms/djangoapps/offline_mode/utils/file_management.py @@ -3,10 +3,16 @@ def get_static_file_path(relative_path): + """ + Constructs the absolute path for a static file based on its relative path. + """ base_path = settings.STATIC_ROOT return os.path.join(base_path, relative_path) def read_static_file(path): + """ + Reads the contents of a static file in binary mode. + """ with open(path, 'rb') as file: return file.read() diff --git a/lms/djangoapps/mobile_api/offline_mode/utils/html_manipulator.py b/lms/djangoapps/offline_mode/utils/html_manipulator.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/utils/html_manipulator.py rename to lms/djangoapps/offline_mode/utils/html_manipulator.py diff --git a/lms/djangoapps/mobile_api/offline_mode/utils/xblock_helpers.py b/lms/djangoapps/offline_mode/utils/xblock_helpers.py similarity index 91% rename from lms/djangoapps/mobile_api/offline_mode/utils/xblock_helpers.py rename to lms/djangoapps/offline_mode/utils/xblock_helpers.py index 53f0008cbec8..e1c7ae26fe74 100644 --- a/lms/djangoapps/mobile_api/offline_mode/utils/xblock_helpers.py +++ b/lms/djangoapps/offline_mode/utils/xblock_helpers.py @@ -12,6 +12,13 @@ User = get_user_model() +OFFLINE_SUPPORTED_XBLOCKS = ['html', 'problem'] + + +def is_offline_supported(xblock): + return xblock.location.block_type in OFFLINE_SUPPORTED_XBLOCKS + + def is_modified(xblock): file_path = f'{base_storage_path(xblock)}content_html.zip' @@ -29,19 +36,6 @@ def generate_request_with_service_user(): request.user = user return request -def enclosing_sequence_for_gating_checks(block): - seq_tags = ['sequential'] - if block.location.block_type in seq_tags: - return None - - ancestor = block - while ancestor and ancestor.location.block_type not in seq_tags: - ancestor = ancestor.get_parent() # Note: CourseBlock's parent is None - - if ancestor: - return block.runtime.get_block(ancestor.location) - return None - def xblock_view_handler(request, xblock, check_if_enrolled=True, disable_staff_debug_info=False): """ @@ -60,7 +54,7 @@ def xblock_view_handler(request, xblock, check_if_enrolled=True, disable_staff_d from openedx.features.course_experience.utils import dates_banner_should_display from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.masquerade import is_masquerading_as_specific_student, setup_masquerade - # from lms.djangoapps.courseware.views.views import get_optimization_flags_for_content + from lms.djangoapps.courseware.views.views import get_optimization_flags_for_content from lms.djangoapps.edxnotes.helpers import is_feature_enabled from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link from common.djangoapps.edxmako.shortcuts import marketing_link, render_to_response, render_to_string @@ -121,7 +115,7 @@ def xblock_view_handler(request, xblock, check_if_enrolled=True, disable_staff_d missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user) fragment = block.render('student_view', context=student_view_context) - # optimization_flags = get_optimization_flags_for_content(block, fragment) + optimization_flags = get_optimization_flags_for_content(block, fragment) context = { 'fragment': fragment, @@ -145,8 +139,7 @@ def xblock_view_handler(request, xblock, check_if_enrolled=True, disable_staff_d 'is_learning_mfe': is_learning_mfe, 'is_mobile_app': is_mobile_app, 'render_course_wide_assets': True, - - # **optimization_flags, + **optimization_flags, } return render_to_string('courseware/courseware-chromeless.html', context) diff --git a/lms/djangoapps/mobile_api/offline_mode/utils/zip_management.py b/lms/djangoapps/offline_mode/utils/zip_management.py similarity index 100% rename from lms/djangoapps/mobile_api/offline_mode/utils/zip_management.py rename to lms/djangoapps/offline_mode/utils/zip_management.py